From 41f239704c71a2c168b04d11a322beb5c5872688 Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Sat, 30 May 2026 14:06:46 +0800 Subject: [PATCH 1/6] fix: release page empty state icon centering and unread filter premature removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Fix Package icon not centered in empty state — missing space between dark:text-text-secondary and mx-auto caused both classes to be invalid 2. In unread-only filter mode, marking a release as read no longer instantly removes it from the list. A snapshot of unread IDs is taken when entering unread mode and on refresh; items stay visible until the next page load Co-Authored-By: Claude Opus 4.8 --- src/components/ReleaseTimeline.tsx | 35 +++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/components/ReleaseTimeline.tsx b/src/components/ReleaseTimeline.tsx index 23ec6529..c35a203e 100644 --- a/src/components/ReleaseTimeline.tsx +++ b/src/components/ReleaseTimeline.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useCallback, useEffect } from 'react'; +import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { Package, Bell, Search, X, RefreshCw, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, LayoutGrid, CalendarDays, ChevronDown, CheckCircle, Filter } from 'lucide-react'; import { Release } from '../types'; import { useAppStore } from '../store/useAppStore'; @@ -57,6 +57,16 @@ export const ReleaseTimeline: React.FC = () => { const [isShowModeDropdownOpen, setIsShowModeDropdownOpen] = useState(false); const [isMarkingAllRead, setIsMarkingAllRead] = useState(false); + // 未读模式下,快照当前未读 release ID,避免标记已读后立即消失 + const unreadSnapshotRef = useRef>(new Set()); + const updateUnreadSnapshot = useCallback(() => { + const ids = new Set(); + subscribedReleases.forEach(r => { + if (!readReleases.has(r.id)) ids.add(r.id); + }); + unreadSnapshotRef.current = ids; + }, [subscribedReleases, readReleases]); + // 使用全局状态的别名,保持代码一致性 const viewMode = releaseViewMode; const selectedFilters = releaseSelectedFilters; @@ -241,13 +251,13 @@ export const ReleaseTimeline: React.FC = () => { })); }, [releasesWithLinks, searchQuery, selectedFilters]); - // 未读模式过滤 + // 未读模式过滤(使用快照,标记已读后不会立即消失,刷新页面后才更新) const filteredReleases = useMemo(() => { if (releaseShowMode === 'unread') { - return preUnreadFilteredReleases.filter(({ release }) => !readReleases.has(release.id)); + return preUnreadFilteredReleases.filter(({ release }) => unreadSnapshotRef.current.has(release.id)); } return preUnreadFilteredReleases; - }, [preUnreadFilteredReleases, releaseShowMode, readReleases]); + }, [preUnreadFilteredReleases, releaseShowMode]); const unreadCount = useMemo(() => { return subscribedReleases.filter(r => !readReleases.has(r.id)).length; @@ -375,6 +385,18 @@ export const ReleaseTimeline: React.FC = () => { } toast(message, actuallyNewCount > 0 ? 'success' : 'info'); + + // 刷新后更新未读快照,以便"仅未读"模式显示最新状态 + if (releaseShowMode === 'unread') { + const state = useAppStore.getState(); + const snapshot = new Set(); + state.releases.forEach(r => { + if (releaseSubscriptions.has(r.repository?.id) && !state.readReleases.has(r.id)) { + snapshot.add(r.id); + } + }); + unreadSnapshotRef.current = snapshot; + } } catch (error) { console.error('Refresh failed:', error); const errorMessage = language === 'zh' @@ -387,6 +409,9 @@ export const ReleaseTimeline: React.FC = () => { }; const handleShowModeChange = (mode: 'all' | 'unread') => { + if (mode === 'unread') { + updateUnreadSnapshot(); + } setReleaseShowMode(mode); setCurrentPage(1); setIsShowModeDropdownOpen(false); @@ -920,7 +945,7 @@ export const ReleaseTimeline: React.FC = () => {
{paginatedReleases.length === 0 ? (
- +

{releaseShowMode === 'unread' ? t('没有未读的 Release', 'No unread releases') From 428fa0b665febdb9798b2c64fb11cd070dd0619b Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Sat, 30 May 2026 14:13:45 +0800 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20address=20CodeRabbit=20review=20?= =?UTF-8?q?=E2=80=94=20TDZ=20error=20and=20consolidate=20snapshot=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Move unreadSnapshotRef/updateUnreadSnapshot after subscribedReleases definition to avoid temporal dead zone ReferenceError 2. Refactor updateUnreadSnapshot to read readReleases via useAppStore.getState() for fresh state access, removing stale closure dependency 3. Replace inline snapshot code in handleRefresh with updateUnreadSnapshot() call Co-Authored-By: Claude Opus 4.8 --- src/components/ReleaseTimeline.tsx | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/components/ReleaseTimeline.tsx b/src/components/ReleaseTimeline.tsx index c35a203e..adb3786f 100644 --- a/src/components/ReleaseTimeline.tsx +++ b/src/components/ReleaseTimeline.tsx @@ -57,16 +57,6 @@ export const ReleaseTimeline: React.FC = () => { const [isShowModeDropdownOpen, setIsShowModeDropdownOpen] = useState(false); const [isMarkingAllRead, setIsMarkingAllRead] = useState(false); - // 未读模式下,快照当前未读 release ID,避免标记已读后立即消失 - const unreadSnapshotRef = useRef>(new Set()); - const updateUnreadSnapshot = useCallback(() => { - const ids = new Set(); - subscribedReleases.forEach(r => { - if (!readReleases.has(r.id)) ids.add(r.id); - }); - unreadSnapshotRef.current = ids; - }, [subscribedReleases, readReleases]); - // 使用全局状态的别名,保持代码一致性 const viewMode = releaseViewMode; const selectedFilters = releaseSelectedFilters; @@ -204,6 +194,17 @@ export const ReleaseTimeline: React.FC = () => { [releases, releaseSubscriptions, includePreRelease] ); + // 未读模式下,快照当前未读 release ID,避免标记已读后立即消失 + const unreadSnapshotRef = useRef>(new Set()); + const updateUnreadSnapshot = useCallback(() => { + const state = useAppStore.getState(); + const ids = new Set(); + subscribedReleases.forEach(r => { + if (!state.readReleases.has(r.id)) ids.add(r.id); + }); + unreadSnapshotRef.current = ids; + }, [subscribedReleases]); + // 预计算每个 release 的下载链接和过滤后的链接 const releasesWithLinks = useMemo(() => { return subscribedReleases.map(release => { @@ -388,14 +389,7 @@ export const ReleaseTimeline: React.FC = () => { // 刷新后更新未读快照,以便"仅未读"模式显示最新状态 if (releaseShowMode === 'unread') { - const state = useAppStore.getState(); - const snapshot = new Set(); - state.releases.forEach(r => { - if (releaseSubscriptions.has(r.repository?.id) && !state.readReleases.has(r.id)) { - snapshot.add(r.id); - } - }); - unreadSnapshotRef.current = snapshot; + updateUnreadSnapshot(); } } catch (error) { console.error('Refresh failed:', error); From eb0a458c639994f243a0535424349f12893fe30a Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Sat, 30 May 2026 14:30:00 +0800 Subject: [PATCH 3/6] fix: new releases not showing in unread-only mode after refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The snapshot update used a useCallback with stale closure — subscribedReleases didn't include newly added releases yet when updateUnreadSnapshot() was called from handleRefresh. Replace the useCallback + manual call pattern with a useEffect that automatically rebuilds the snapshot whenever releases, releaseSubscriptions, includePreRelease, or readReleases change. This ensures the snapshot is always in sync with the current state. Co-Authored-By: Claude Opus 4.8 --- src/components/ReleaseTimeline.tsx | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/components/ReleaseTimeline.tsx b/src/components/ReleaseTimeline.tsx index adb3786f..bbec1fc3 100644 --- a/src/components/ReleaseTimeline.tsx +++ b/src/components/ReleaseTimeline.tsx @@ -195,15 +195,20 @@ export const ReleaseTimeline: React.FC = () => { ); // 未读模式下,快照当前未读 release ID,避免标记已读后立即消失 + // 依赖项变化时自动重建快照(releases 刷新、标记已读等) const unreadSnapshotRef = useRef>(new Set()); - const updateUnreadSnapshot = useCallback(() => { + useEffect(() => { const state = useAppStore.getState(); const ids = new Set(); - subscribedReleases.forEach(r => { - if (!state.readReleases.has(r.id)) ids.add(r.id); + releases.forEach(r => { + if (releaseSubscriptions.has(r.repository.id) && + (includePreRelease || !r.prerelease) && + !state.readReleases.has(r.id)) { + ids.add(r.id); + } }); unreadSnapshotRef.current = ids; - }, [subscribedReleases]); + }, [releases, releaseSubscriptions, includePreRelease, readReleases]); // 预计算每个 release 的下载链接和过滤后的链接 const releasesWithLinks = useMemo(() => { @@ -386,11 +391,6 @@ export const ReleaseTimeline: React.FC = () => { } toast(message, actuallyNewCount > 0 ? 'success' : 'info'); - - // 刷新后更新未读快照,以便"仅未读"模式显示最新状态 - if (releaseShowMode === 'unread') { - updateUnreadSnapshot(); - } } catch (error) { console.error('Refresh failed:', error); const errorMessage = language === 'zh' @@ -403,9 +403,6 @@ export const ReleaseTimeline: React.FC = () => { }; const handleShowModeChange = (mode: 'all' | 'unread') => { - if (mode === 'unread') { - updateUnreadSnapshot(); - } setReleaseShowMode(mode); setCurrentPage(1); setIsShowModeDropdownOpen(false); From e62d2e2d14ec2bf0fe7b3a3d1244a5ae51138148 Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Sat, 30 May 2026 14:34:09 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20persist=20releaseShowMode=20(?= =?UTF-8?q?=E5=85=A8=E9=83=A8/=E4=BB=85=E6=9C=AA=E8=AF=BB)=20across=20page?= =?UTF-8?q?=20switches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move releaseShowMode from local useState to Zustand store with IndexedDB persistence, matching the pattern used by releaseViewMode and other release page settings. Co-Authored-By: Claude Opus 4.8 --- src/components/ReleaseTimeline.tsx | 3 ++- src/store/useAppStore.ts | 5 +++++ src/types/index.ts | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/ReleaseTimeline.tsx b/src/components/ReleaseTimeline.tsx index bbec1fc3..47e9184e 100644 --- a/src/components/ReleaseTimeline.tsx +++ b/src/components/ReleaseTimeline.tsx @@ -40,6 +40,8 @@ export const ReleaseTimeline: React.FC = () => { setReleaseIsRefreshing, includePreRelease, setIncludePreRelease, + releaseShowMode, + setReleaseShowMode, } = useAppStore(); const { toast, confirm } = useDialog(); @@ -53,7 +55,6 @@ export const ReleaseTimeline: React.FC = () => { const [fullContentReleases, setFullContentReleases] = useState>(new Set()); // 视图切换下拉菜单状态(本地UI状态) const [isViewDropdownOpen, setIsViewDropdownOpen] = useState(false); - const [releaseShowMode, setReleaseShowMode] = useState<'all' | 'unread'>('all'); const [isShowModeDropdownOpen, setIsShowModeDropdownOpen] = useState(false); const [isMarkingAllRead, setIsMarkingAllRead] = useState(false); diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index 43dd30ef..a1b03fe3 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -262,6 +262,7 @@ type PersistedAppState = Partial< | 'forkSearchQuery' | 'forkExpandedRepositories' | 'releaseViewMode' + | 'releaseShowMode' | 'releaseSelectedFilters' | 'releaseSearchQuery' | 'includePreRelease' @@ -381,6 +382,7 @@ const normalizePersistedState = ( language: safePersisted.language || 'zh', isAuthenticated: !!(safePersisted.user && safePersisted.githubToken), releaseViewMode: safePersisted.releaseViewMode || 'timeline', + releaseShowMode: safePersisted.releaseShowMode === 'unread' ? 'unread' : 'all', releaseSelectedFilters: Array.isArray(safePersisted.releaseSelectedFilters) ? safePersisted.releaseSelectedFilters : [], releaseSearchQuery: typeof safePersisted.releaseSearchQuery === 'string' ? safePersisted.releaseSearchQuery : '', discoveryChannels: (() => { @@ -738,6 +740,7 @@ export const useAppStore = create()( isSidebarCollapsed: false, readmeModalOpen: false, releaseViewMode: 'timeline', + releaseShowMode: 'all', releaseSelectedFilters: [], releaseSearchQuery: '', releaseExpandedRepositories: new Set(), @@ -1265,6 +1268,7 @@ export const useAppStore = create()( // Release Timeline View actions setReleaseViewMode: (releaseViewMode) => set({ releaseViewMode }), + setReleaseShowMode: (releaseShowMode) => set({ releaseShowMode }), setReleaseSelectedFilters: (releaseSelectedFilters) => set({ releaseSelectedFilters }), toggleReleaseSelectedFilter: (filterId) => set((state) => ({ releaseSelectedFilters: state.releaseSelectedFilters.includes(filterId) @@ -1474,6 +1478,7 @@ export const useAppStore = create()( // 持久化Release页面视图设置 releaseViewMode: state.releaseViewMode, + releaseShowMode: state.releaseShowMode, releaseSelectedFilters: state.releaseSelectedFilters, releaseSearchQuery: state.releaseSearchQuery, releaseExpandedRepositories: Array.from(state.releaseExpandedRepositories), diff --git a/src/types/index.ts b/src/types/index.ts index c511ce35..0658f714 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -275,6 +275,7 @@ export interface AppState { // Release Timeline View releaseViewMode: 'timeline' | 'repository'; + releaseShowMode: 'all' | 'unread'; releaseSelectedFilters: string[]; releaseSearchQuery: string; releaseExpandedRepositories: Set; From c4ed7f9ae85731ee51224103e384be7ed3310fe6 Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Sat, 30 May 2026 14:49:39 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20address=20CodeRabbit=20review=20?= =?UTF-8?q?=E2=80=94=20snapshot=20dependency=20and=20missing=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Replace readReleases with releaseShowMode in useEffect dependency array to avoid rebuilding snapshot on mark-as-read (which would defeat the purpose of the snapshot); rebuild on mode switch instead 2. Add setReleaseShowMode to AppActions interface for type completeness Co-Authored-By: Claude Opus 4.8 --- src/components/ReleaseTimeline.tsx | 2 +- src/store/useAppStore.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ReleaseTimeline.tsx b/src/components/ReleaseTimeline.tsx index 47e9184e..0ee54bd4 100644 --- a/src/components/ReleaseTimeline.tsx +++ b/src/components/ReleaseTimeline.tsx @@ -209,7 +209,7 @@ export const ReleaseTimeline: React.FC = () => { } }); unreadSnapshotRef.current = ids; - }, [releases, releaseSubscriptions, includePreRelease, readReleases]); + }, [releases, releaseSubscriptions, includePreRelease, releaseShowMode]); // 预计算每个 release 的下载链接和过滤后的链接 const releasesWithLinks = useMemo(() => { diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index a1b03fe3..5e64f2a6 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -174,6 +174,7 @@ interface AppActions { // Release Timeline View actions setReleaseViewMode: (mode: 'timeline' | 'repository') => void; + setReleaseShowMode: (mode: 'all' | 'unread') => void; setReleaseSelectedFilters: (filters: string[]) => void; toggleReleaseSelectedFilter: (filterId: string) => void; clearReleaseSelectedFilters: () => void; From e1320fc0c8ac849306f26d003c22dd48efed836d Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Sat, 30 May 2026 15:00:54 +0800 Subject: [PATCH 6/6] fix: new releases not appearing in unread-only mode after refresh The useEffect updates unreadSnapshotRef but refs don't trigger useMemo recalculation. Add snapshotVersion state as a bridge: the effect increments it after updating the ref, and filteredReleases depends on it to recompute. The effect deliberately excludes readReleases from its dependency array so that marking an item as read does NOT rebuild the snapshot (preserving the 'items stay visible until next refresh' behavior). Co-Authored-By: Claude Opus 4.8 --- src/components/ReleaseTimeline.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/ReleaseTimeline.tsx b/src/components/ReleaseTimeline.tsx index 0ee54bd4..146e399f 100644 --- a/src/components/ReleaseTimeline.tsx +++ b/src/components/ReleaseTimeline.tsx @@ -196,8 +196,9 @@ export const ReleaseTimeline: React.FC = () => { ); // 未读模式下,快照当前未读 release ID,避免标记已读后立即消失 - // 依赖项变化时自动重建快照(releases 刷新、标记已读等) + // 不依赖 readReleases,避免标记已读时重建快照导致列表项立即消失 const unreadSnapshotRef = useRef>(new Set()); + const [snapshotVersion, setSnapshotVersion] = useState(0); useEffect(() => { const state = useAppStore.getState(); const ids = new Set(); @@ -209,6 +210,7 @@ export const ReleaseTimeline: React.FC = () => { } }); unreadSnapshotRef.current = ids; + setSnapshotVersion(v => v + 1); }, [releases, releaseSubscriptions, includePreRelease, releaseShowMode]); // 预计算每个 release 的下载链接和过滤后的链接 @@ -264,7 +266,9 @@ export const ReleaseTimeline: React.FC = () => { return preUnreadFilteredReleases.filter(({ release }) => unreadSnapshotRef.current.has(release.id)); } return preUnreadFilteredReleases; - }, [preUnreadFilteredReleases, releaseShowMode]); + // snapshotVersion 触发快照更新后重算;readReleases 不在此处以避免标记已读立即消失 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [preUnreadFilteredReleases, releaseShowMode, snapshotVersion]); const unreadCount = useMemo(() => { return subscribedReleases.filter(r => !readReleases.has(r.id)).length;