-
Notifications
You must be signed in to change notification settings - Fork 148
fix: keep PR #90 and avoid mac desktop white screen crash #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
856c4cd
535acec
80cc3d9
3ddadbc
1987123
0012fb3
2a7653f
afcf451
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -232,6 +232,7 @@ const normalizePersistedState = ( | |
| currentState: AppState & AppActions | ||
| ): Partial<AppState & AppActions> => { | ||
| const safePersisted = persisted ?? {}; | ||
| const defaultDiscoveryChannelIds = new Set(defaultDiscoveryChannels.map((channel) => channel.id)); | ||
|
|
||
| const repositories = Array.isArray(safePersisted.repositories) ? safePersisted.repositories : []; | ||
| const releases = Array.isArray(safePersisted.releases) ? safePersisted.releases : []; | ||
|
|
@@ -273,27 +274,71 @@ const normalizePersistedState = ( | |
| releaseViewMode: safePersisted.releaseViewMode || 'timeline', | ||
| releaseSelectedFilters: Array.isArray(safePersisted.releaseSelectedFilters) ? safePersisted.releaseSelectedFilters : [], | ||
| releaseSearchQuery: typeof safePersisted.releaseSearchQuery === 'string' ? safePersisted.releaseSearchQuery : '', | ||
| discoveryChannels: (() => { | ||
| const persisted = (safePersisted as Record<string, unknown>).discoveryChannels; | ||
| if (!Array.isArray(persisted)) return defaultDiscoveryChannels; | ||
|
|
||
| return defaultDiscoveryChannels.map((defaultChannel) => { | ||
| const persistedChannel = persisted.find((channel: unknown) => { | ||
| return (channel as Record<string, unknown>)?.id === defaultChannel.id; | ||
| }) as Record<string, unknown> | undefined; | ||
|
|
||
| if (!persistedChannel) { | ||
| return defaultChannel; | ||
| } | ||
|
|
||
| return { | ||
| ...defaultChannel, | ||
| ...(persistedChannel as Partial<typeof defaultChannel>), | ||
| enabled: persistedChannel.enabled !== false, | ||
| }; | ||
| }); | ||
|
Comment on lines
+310
to
+328
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sanitize persisted discovery channel fields before rendering them. The code validates channel IDs but then spreads arbitrary persisted fields over defaults. A malformed persisted value like 🛡️ Proposed sanitization pattern return defaultDiscoveryChannels.map((defaultChannel) => {
const persistedChannel = persisted.find((channel: unknown) => {
return (channel as Record<string, unknown>)?.id === defaultChannel.id;
}) as Record<string, unknown> | undefined;
if (!persistedChannel) {
return defaultChannel;
}
return {
...defaultChannel,
- ...(persistedChannel as Partial<typeof defaultChannel>),
+ name: typeof persistedChannel.name === 'string' ? persistedChannel.name : defaultChannel.name,
+ nameEn: typeof persistedChannel.nameEn === 'string' ? persistedChannel.nameEn : defaultChannel.nameEn,
+ description: typeof persistedChannel.description === 'string' ? persistedChannel.description : defaultChannel.description,
enabled: persistedChannel.enabled !== false,
};
});Apply the same sanitization in the Also applies to: 1307-1325 🤖 Prompt for AI Agents |
||
| })(), | ||
| discoveryRepos: (() => { | ||
| const persisted = (safePersisted as Record<string, unknown>).discoveryRepos; | ||
| if (persisted && typeof persisted === 'object' && !Array.isArray(persisted)) { | ||
| return persisted as Record<DiscoveryChannelId, DiscoveryRepo[]>; | ||
| return { | ||
| 'trending': [], | ||
| 'hot-release': [], | ||
| 'most-popular': [], | ||
| 'topic': [], | ||
| 'search': [], | ||
| ...(persisted as Record<DiscoveryChannelId, DiscoveryRepo[]>), | ||
| }; | ||
| } | ||
| return { 'trending': [], 'hot-release': [], 'most-popular': [], 'topic': [], 'search': [] } as Record<DiscoveryChannelId, DiscoveryRepo[]>; | ||
| })(), | ||
| discoveryLastRefresh: (() => { | ||
| const persisted = (safePersisted as Record<string, unknown>).discoveryLastRefresh; | ||
| if (persisted && typeof persisted === 'object' && !Array.isArray(persisted)) { | ||
| return persisted as Record<string, string | null>; | ||
| return { | ||
| 'trending': null, | ||
| 'hot-release': null, | ||
| 'most-popular': null, | ||
| 'topic': null, | ||
| 'search': null, | ||
| ...(persisted as Record<string, string | null>), | ||
| }; | ||
| } | ||
| return { 'trending': null, 'hot-release': null, 'most-popular': null, 'topic': null, 'search': null }; | ||
| })(), | ||
| discoveryTotalCount: (() => { | ||
| const persisted = (safePersisted as Record<string, unknown>).discoveryTotalCount; | ||
| if (persisted && typeof persisted === 'object' && !Array.isArray(persisted)) { | ||
| return persisted as Record<string, number>; | ||
| return { | ||
| 'trending': 0, | ||
| 'hot-release': 0, | ||
| 'most-popular': 0, | ||
| 'topic': 0, | ||
| 'search': 0, | ||
| ...(persisted as Record<string, number>), | ||
| }; | ||
| } | ||
| return { 'trending': 0, 'hot-release': 0, 'most-popular': 0, 'topic': 0, 'search': 0 }; | ||
| })(), | ||
|
Comment on lines
330
to
371
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per-key values in discovery maps are not validated; malformed entries can still crash downstream. The normalization here only checks that the persisted value is a non-array object and then spreads it over the defaults. If a persisted entry has an unexpected per-key type (e.g. Given the PR's hardening goal, consider validating each channel's value type (e.g., coerce non-arrays to 🛡️ Sketch of per-key validation discoveryRepos: (() => {
const persisted = (safePersisted as Record<string, unknown>).discoveryRepos;
- if (persisted && typeof persisted === 'object' && !Array.isArray(persisted)) {
- return {
- 'trending': [],
- 'hot-release': [],
- 'most-popular': [],
- 'topic': [],
- 'search': [],
- ...(persisted as Record<DiscoveryChannelId, DiscoveryRepo[]>),
- };
- }
- return { 'trending': [], 'hot-release': [], 'most-popular': [], 'topic': [], 'search': [] } as Record<DiscoveryChannelId, DiscoveryRepo[]>;
+ const base: Record<DiscoveryChannelId, DiscoveryRepo[]> = {
+ 'trending': [], 'hot-release': [], 'most-popular': [], 'topic': [], 'search': [],
+ };
+ if (persisted && typeof persisted === 'object' && !Array.isArray(persisted)) {
+ for (const key of Object.keys(base) as DiscoveryChannelId[]) {
+ const v = (persisted as Record<string, unknown>)[key];
+ if (Array.isArray(v)) base[key] = v as DiscoveryRepo[];
+ }
+ }
+ return base;
})(),(Apply the same pattern to 🤖 Prompt for AI Agents |
||
| selectedDiscoveryChannel: defaultDiscoveryChannelIds.has(safePersisted.selectedDiscoveryChannel as DiscoveryChannelId) | ||
| ? safePersisted.selectedDiscoveryChannel as DiscoveryChannelId | ||
| : 'trending', | ||
| // 确保 subscription 相关状态包含 trending 键 | ||
| subscriptionRepos: { | ||
| 'most-stars': [], | ||
|
|
@@ -1221,6 +1266,26 @@ export const useAppStore = create<AppState & AppActions>()( | |
| if (state && !state.selectedDiscoveryChannel) { | ||
| state.selectedDiscoveryChannel = 'trending'; | ||
| } | ||
| if (state && (!state.discoveryChannels || !Array.isArray(state.discoveryChannels))) { | ||
| state.discoveryChannels = defaultDiscoveryChannels; | ||
| } else if (state && Array.isArray(state.discoveryChannels)) { | ||
| const persistedChannels = state.discoveryChannels as unknown[]; | ||
| state.discoveryChannels = defaultDiscoveryChannels.map((defaultChannel) => { | ||
| const persistedChannel = persistedChannels.find((channel) => { | ||
| return (channel as Record<string, unknown>)?.id === defaultChannel.id; | ||
| }) as Record<string, unknown> | undefined; | ||
|
|
||
| if (!persistedChannel) { | ||
| return defaultChannel; | ||
| } | ||
|
|
||
| return { | ||
| ...defaultChannel, | ||
| ...(persistedChannel as Partial<typeof defaultChannel>), | ||
| enabled: persistedChannel.enabled !== false, | ||
| }; | ||
| }); | ||
| } | ||
| // 迁移订阅频道(版本 4→5:daily-dev → most-dev,新增 trending,补全 nameEn) | ||
| const defaultChannelsMap = new Map(defaultSubscriptionChannels.map(ch => [ch.id, ch])); | ||
| if (state && !Array.isArray(state.subscriptionChannels)) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t jump to unloaded pages while only fetching one next page.
The page-jump controls can request page N, but
handlePageChangeonly appendscurrentNextPageonce. Jumping from page 1 to page 10 leavescurrentPageReposempty because pages 3–10 were never loaded.🐛 Proposed guard
const handlePageChange = useCallback((page: number) => { - setCurrentPage(page); + const loadedPages = Math.ceil(allRepos.length / ITEMS_PER_PAGE); + const targetPage = page > loadedPages + 1 ? loadedPages + 1 : page; + setCurrentPage(targetPage); // 如果目标页的数据还没有加载,先加载数据 - const requiredItems = page * ITEMS_PER_PAGE; + const requiredItems = targetPage * ITEMS_PER_PAGE; if (allRepos.length < requiredItems && currentHasMore && !currentIsLoading) { refreshChannel(selectedDiscoveryChannel, currentNextPage, true); }Alternatively, store repos by page and fetch the requested page directly instead of appending into a flat list.
📝 Committable suggestion
🤖 Prompt for AI Agents