diff --git a/src/components/ReleaseTimeline.tsx b/src/components/ReleaseTimeline.tsx index 23ec6529..146e399f 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'; @@ -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); @@ -194,6 +195,24 @@ export const ReleaseTimeline: React.FC = () => { [releases, releaseSubscriptions, includePreRelease] ); + // 未读模式下,快照当前未读 release ID,避免标记已读后立即消失 + // 不依赖 readReleases,避免标记已读时重建快照导致列表项立即消失 + const unreadSnapshotRef = useRef>(new Set()); + const [snapshotVersion, setSnapshotVersion] = useState(0); + useEffect(() => { + const state = useAppStore.getState(); + const ids = new Set(); + releases.forEach(r => { + if (releaseSubscriptions.has(r.repository.id) && + (includePreRelease || !r.prerelease) && + !state.readReleases.has(r.id)) { + ids.add(r.id); + } + }); + unreadSnapshotRef.current = ids; + setSnapshotVersion(v => v + 1); + }, [releases, releaseSubscriptions, includePreRelease, releaseShowMode]); + // 预计算每个 release 的下载链接和过滤后的链接 const releasesWithLinks = useMemo(() => { return subscribedReleases.map(release => { @@ -241,13 +260,15 @@ 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]); + // 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; @@ -920,7 +941,7 @@ export const ReleaseTimeline: React.FC = () => {
{paginatedReleases.length === 0 ? (
- +

{releaseShowMode === 'unread' ? t('没有未读的 Release', 'No unread releases') diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index 43dd30ef..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; @@ -262,6 +263,7 @@ type PersistedAppState = Partial< | 'forkSearchQuery' | 'forkExpandedRepositories' | 'releaseViewMode' + | 'releaseShowMode' | 'releaseSelectedFilters' | 'releaseSearchQuery' | 'includePreRelease' @@ -381,6 +383,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 +741,7 @@ export const useAppStore = create()( isSidebarCollapsed: false, readmeModalOpen: false, releaseViewMode: 'timeline', + releaseShowMode: 'all', releaseSelectedFilters: [], releaseSearchQuery: '', releaseExpandedRepositories: new Set(), @@ -1265,6 +1269,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 +1479,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;