-
Notifications
You must be signed in to change notification settings - Fork 80
fix: preserve repository view state and stabilize sync interactions #60
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 all commits
a2a7113
6ceeb50
99545c2
3ad9067
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 |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import React, { useState, useRef, useEffect } from 'react'; | ||
| import React, { useState, useRef, useEffect, useMemo } from 'react'; | ||
| import { Bot, ChevronDown, Pause, Play } from 'lucide-react'; | ||
| import { RepositoryCard } from './RepositoryCard'; | ||
|
|
||
|
|
@@ -34,6 +34,9 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({ | |
| const [showDropdown, setShowDropdown] = useState(false); | ||
| const [isPaused, setIsPaused] = useState(false); | ||
| const [disableCardAnimations, setDisableCardAnimations] = useState(false); | ||
| const previousCategoryRef = useRef(selectedCategory); | ||
| const savedScrollYRef = useRef<number | null>(null); | ||
| const restoreScrollFrameRef = useRef<number | null>(null); | ||
|
Comment on lines
+37
to
+39
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. Guard scroll restoration when category changes during sync. Current logic always restores saved scroll on sync end. If the user switches category while syncing (Line 124 scrolls to top), the later restore can jump back to the old position and break the new-category flow. 🔧 Proposed fix const previousCategoryRef = useRef(selectedCategory);
const savedScrollYRef = useRef<number | null>(null);
const restoreScrollFrameRef = useRef<number | null>(null);
+ const syncStartCategoryRef = useRef<string | null>(null);
@@
if (isSyncing) {
+ syncStartCategoryRef.current = previousCategoryRef.current;
savedScrollYRef.current = window.scrollY;
if (restoreScrollFrameRef.current !== null) {
cancelAnimationFrame(restoreScrollFrameRef.current);
restoreScrollFrameRef.current = null;
}
return;
}
const targetScrollY = savedScrollYRef.current;
if (targetScrollY === null) return;
+ if (
+ syncStartCategoryRef.current !== null &&
+ syncStartCategoryRef.current !== previousCategoryRef.current
+ ) {
+ savedScrollYRef.current = null;
+ syncStartCategoryRef.current = null;
+ return;
+ }
restoreScrollFrameRef.current = window.requestAnimationFrame(() => {
restoreScrollFrameRef.current = window.requestAnimationFrame(() => {
window.scrollTo({ top: targetScrollY, behavior: 'auto' });
restoreScrollFrameRef.current = null;
savedScrollYRef.current = null;
+ syncStartCategoryRef.current = null;
});
});Also applies to: 163-184 🤖 Prompt for AI Agents |
||
|
|
||
| // 使用 useRef 来管理停止状态,确保在异步操作中能正确访问最新值 | ||
| const shouldStopRef = useRef(false); | ||
|
|
@@ -85,12 +88,52 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({ | |
| const startIndex = filteredRepositories.length === 0 ? 0 : 1; | ||
| const endIndex = Math.min(visibleCount, filteredRepositories.length); | ||
| const visibleRepositories = filteredRepositories.slice(0, visibleCount); | ||
|
|
||
| // Reset visible count when filters or data change | ||
| const filterResetKey = useMemo(() => JSON.stringify({ | ||
| selectedCategory, | ||
| query: searchFilters.query, | ||
| languages: searchFilters.languages, | ||
| tags: searchFilters.tags, | ||
| platforms: searchFilters.platforms, | ||
| sortBy: searchFilters.sortBy, | ||
| sortOrder: searchFilters.sortOrder, | ||
| minStars: searchFilters.minStars, | ||
| maxStars: searchFilters.maxStars, | ||
| isAnalyzed: searchFilters.isAnalyzed, | ||
| isSubscribed: searchFilters.isSubscribed, | ||
| }), [ | ||
| selectedCategory, | ||
| searchFilters.query, | ||
| searchFilters.languages, | ||
| searchFilters.tags, | ||
| searchFilters.platforms, | ||
| searchFilters.sortBy, | ||
| searchFilters.sortOrder, | ||
| searchFilters.minStars, | ||
| searchFilters.maxStars, | ||
| searchFilters.isAnalyzed, | ||
| searchFilters.isSubscribed, | ||
| ]); | ||
|
|
||
| // Reset visible count only when filter context changes. | ||
| useEffect(() => { | ||
| setVisibleCount(LOAD_BATCH); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [selectedCategory, repositories, filteredRepositories.length]); | ||
| }, [filterResetKey]); | ||
|
|
||
| useEffect(() => { | ||
| if (previousCategoryRef.current !== selectedCategory) { | ||
| window.scrollTo({ top: 0, behavior: 'auto' }); | ||
| previousCategoryRef.current = selectedCategory; | ||
| } | ||
| }, [selectedCategory]); | ||
|
|
||
| // Clamp visible count when result set becomes smaller, but do not collapse | ||
| // back to the initial batch during backend sync refreshes. | ||
| useEffect(() => { | ||
| setVisibleCount((count) => { | ||
| if (filteredRepositories.length === 0) return LOAD_BATCH; | ||
| return Math.min(count, filteredRepositories.length); | ||
| }); | ||
| }, [filteredRepositories.length]); | ||
|
|
||
| // IntersectionObserver to load more on demand | ||
| useEffect(() => { | ||
|
|
@@ -117,11 +160,35 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({ | |
| useEffect(() => { | ||
| const handleSyncVisualState = (event: Event) => { | ||
| const customEvent = event as CustomEvent<{ isSyncing?: boolean }>; | ||
| setDisableCardAnimations(!!customEvent.detail?.isSyncing); | ||
| const isSyncing = !!customEvent.detail?.isSyncing; | ||
| setDisableCardAnimations(isSyncing); | ||
|
|
||
| if (isSyncing) { | ||
| savedScrollYRef.current = window.scrollY; | ||
| if (restoreScrollFrameRef.current !== null) { | ||
| cancelAnimationFrame(restoreScrollFrameRef.current); | ||
| restoreScrollFrameRef.current = null; | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| const targetScrollY = savedScrollYRef.current; | ||
| if (targetScrollY === null) return; | ||
|
|
||
| restoreScrollFrameRef.current = window.requestAnimationFrame(() => { | ||
| restoreScrollFrameRef.current = window.requestAnimationFrame(() => { | ||
| window.scrollTo({ top: targetScrollY, behavior: 'auto' }); | ||
| restoreScrollFrameRef.current = null; | ||
| savedScrollYRef.current = null; | ||
| }); | ||
| }); | ||
| }; | ||
|
|
||
| window.addEventListener('gsm:repository-sync-visual-state', handleSyncVisualState as EventListener); | ||
| return () => { | ||
| if (restoreScrollFrameRef.current !== null) { | ||
| cancelAnimationFrame(restoreScrollFrameRef.current); | ||
| } | ||
| window.removeEventListener('gsm:repository-sync-visual-state', handleSyncVisualState as EventListener); | ||
| }; | ||
| }, []); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,6 +71,7 @@ interface AppActions { | |
| // UI actions | ||
| setTheme: (theme: 'light' | 'dark') => void; | ||
| setCurrentView: (view: 'repositories' | 'releases' | 'settings') => void; | ||
| setSelectedCategory: (category: string) => void; | ||
| setLanguage: (language: 'zh' | 'en') => void; | ||
|
|
||
| // Update actions | ||
|
|
@@ -112,6 +113,8 @@ type PersistedAppState = Partial< | |
| | 'customCategories' | ||
| | 'assetFilters' | ||
| | 'theme' | ||
| | 'currentView' | ||
| | 'selectedCategory' | ||
|
Comment on lines
+116
to
+117
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. Validate persisted Persisting category id is great, but stale/removed custom category ids can be restored and lead to empty repository results (see 🔧 Proposed fix const normalizePersistedState = (
persisted: PersistedAppState | undefined,
currentState: AppState & AppActions
): Partial<AppState & AppActions> => {
const safePersisted = persisted ?? {};
+ const persistedCustomCategories = Array.isArray(safePersisted.customCategories)
+ ? safePersisted.customCategories
+ : [];
+ const validCategoryIds = new Set([
+ ...defaultCategories.map((cat) => cat.id),
+ ...persistedCustomCategories.map((cat) => cat.id),
+ ]);
+ const normalizedSelectedCategory =
+ typeof safePersisted.selectedCategory === 'string' &&
+ validCategoryIds.has(safePersisted.selectedCategory)
+ ? safePersisted.selectedCategory
+ : 'all';
+
const repositories = Array.isArray(safePersisted.repositories) ? safePersisted.repositories : [];
const releases = Array.isArray(safePersisted.releases) ? safePersisted.releases : [];
@@
- customCategories: Array.isArray(safePersisted.customCategories) ? safePersisted.customCategories : [],
+ customCategories: persistedCustomCategories,
+ selectedCategory: normalizedSelectedCategory,Also applies to: 484-485 🤖 Prompt for AI Agents |
||
| | 'language' | ||
| | 'searchFilters' | ||
| > | ||
|
|
@@ -276,6 +279,7 @@ export const useAppStore = create<AppState & AppActions>()( | |
| assetFilters: [], | ||
| theme: 'light', | ||
| currentView: 'repositories', | ||
| selectedCategory: 'all', | ||
| language: 'zh', | ||
| updateNotification: null, | ||
| analysisProgress: { current: 0, total: 0 }, | ||
|
|
@@ -429,6 +433,7 @@ export const useAppStore = create<AppState & AppActions>()( | |
| // UI actions | ||
| setTheme: (theme) => set({ theme }), | ||
| setCurrentView: (currentView) => set({ currentView }), | ||
| setSelectedCategory: (selectedCategory) => set({ selectedCategory }), | ||
| setLanguage: (language) => set({ language }), | ||
|
|
||
| // Update actions | ||
|
|
@@ -476,6 +481,8 @@ export const useAppStore = create<AppState & AppActions>()( | |
|
|
||
| // 持久化UI设置 | ||
| theme: state.theme, | ||
| currentView: state.currentView, | ||
| selectedCategory: state.selectedCategory, | ||
| language: state.language, | ||
|
|
||
| // backendApiSecret: 保留在内存中,不持久化(安全考虑) | ||
|
|
||
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.
Use a base-aware icon path to avoid broken logo URLs.
Line 72 uses a relative path (
"./icon.png"). On non-root paths this can resolve incorrectly and 404. Prefer a base-aware absolute asset path.🔧 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents