diff --git a/src/components/PostList.tsx b/src/components/PostList.tsx index 7ea2f0d..fbd9946 100644 --- a/src/components/PostList.tsx +++ b/src/components/PostList.tsx @@ -37,6 +37,8 @@ interface PostListProps { loadingMore?: boolean; /** Whether there are more posts available to load */ hasMore?: boolean; + /** Increment this to reset selection and scroll to top (e.g., on refresh) */ + refreshKey?: number; } /** @@ -58,11 +60,14 @@ export function PostList({ onLoadMore, loadingMore = false, hasMore = true, + refreshKey, }: PostListProps) { const scrollRef = useRef(null); // Save scroll position so we can restore when refocused const savedScrollTop = useRef(0); const wasFocused = useRef(focused); + // Track previous refreshKey to detect when refresh is triggered + const prevRefreshKey = useRef(refreshKey); // Restore scroll position when gaining focus useEffect(() => { @@ -77,7 +82,7 @@ export function PostList({ wasFocused.current = focused; }, [focused]); - const { selectedIndex } = useListNavigation({ + const { selectedIndex, setSelectedIndex } = useListNavigation({ itemCount: posts.length, enabled: focused, onSelect: (index) => { @@ -93,6 +98,24 @@ export function PostList({ }, }); + // Reset to top when refreshKey changes (user explicitly triggered refresh) + useEffect(() => { + // Skip on initial mount (prevRefreshKey.current will equal refreshKey) + if ( + prevRefreshKey.current !== undefined && + refreshKey !== undefined && + refreshKey !== prevRefreshKey.current + ) { + setSelectedIndex(0); + if (scrollRef.current) { + scrollRef.current.scrollTo(0); + } + savedScrollTop.current = 0; + } + + prevRefreshKey.current = refreshKey; + }, [refreshKey, setSelectedIndex]); + // Notify parent of selection changes (e.g., for collapsible headers) useEffect(() => { onSelectedIndexChange?.(selectedIndex); diff --git a/src/experiments/TimelineScreenExperimental.tsx b/src/experiments/TimelineScreenExperimental.tsx index b9f8147..2f082b7 100644 --- a/src/experiments/TimelineScreenExperimental.tsx +++ b/src/experiments/TimelineScreenExperimental.tsx @@ -7,7 +7,7 @@ */ import { useKeyboard } from "@opentui/react"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import type { XClient } from "@/api/client"; import type { TweetData } from "@/api/types"; @@ -130,6 +130,9 @@ export function TimelineScreenExperimental({ initialTab: preferences.timeline.default_tab, }); + // Track refresh to reset PostList selection/scroll + const [refreshKey, setRefreshKey] = useState(0); + // Report post count to parent useEffect(() => { onPostCountChange?.(posts.length); @@ -157,6 +160,7 @@ export function TimelineScreenExperimental({ break; case "r": refresh(); + setRefreshKey((k) => k + 1); break; } }); @@ -212,6 +216,7 @@ export function TimelineScreenExperimental({ onLoadMore={fetchNextPage} loadingMore={isFetchingNextPage} hasMore={hasNextPage} + refreshKey={refreshKey} /> ); diff --git a/src/screens/BookmarksScreen.tsx b/src/screens/BookmarksScreen.tsx index 0dc0b63..eea6b88 100644 --- a/src/screens/BookmarksScreen.tsx +++ b/src/screens/BookmarksScreen.tsx @@ -5,7 +5,7 @@ */ import { useKeyboard } from "@opentui/react"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import type { XClient } from "@/api/client"; import type { BookmarkFolder, TweetData } from "@/api/types"; @@ -101,6 +101,9 @@ export function BookmarksScreen({ fetchNextPage, } = useBookmarksQuery({ client, folderId: selectedFolder?.id }); + // Track refresh to reset PostList selection/scroll + const [refreshKey, setRefreshKey] = useState(0); + // Report post count to parent useEffect(() => { onPostCountChange?.(posts.length); @@ -117,6 +120,7 @@ export function BookmarksScreen({ if (key.name === "r") { refresh(); + setRefreshKey((k) => k + 1); } // Open folder picker with 'f' @@ -192,6 +196,7 @@ export function BookmarksScreen({ onLoadMore={fetchNextPage} loadingMore={isFetchingNextPage} hasMore={hasNextPage} + refreshKey={refreshKey} /> ); diff --git a/src/screens/ProfileScreen.tsx b/src/screens/ProfileScreen.tsx index 2a7592f..0d1c02a 100644 --- a/src/screens/ProfileScreen.tsx +++ b/src/screens/ProfileScreen.tsx @@ -173,6 +173,9 @@ export function ProfileScreen({ const [activeTab, setActiveTab] = useState("tweets"); + // Track refresh to reset PostList selection/scroll + const [refreshKey, setRefreshKey] = useState(0); + // Get current tab index for arrow navigation const activeTabIndex = useMemo( () => availableTabs.findIndex((t) => t.key === activeTab), @@ -289,6 +292,7 @@ export function ProfileScreen({ break; case "r": refresh(); + setRefreshKey((k) => k + 1); break; case "a": // Open avatar/profile photo in Quick Look @@ -769,6 +773,7 @@ export function ProfileScreen({ onBookmark={onBookmark} getActionState={getActionState} initActionState={initActionState} + refreshKey={refreshKey} /> ) : ( diff --git a/src/screens/TimelineScreen.tsx b/src/screens/TimelineScreen.tsx index 83077c4..a505cd6 100644 --- a/src/screens/TimelineScreen.tsx +++ b/src/screens/TimelineScreen.tsx @@ -4,7 +4,7 @@ */ import { useKeyboard } from "@opentui/react"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import type { XClient } from "@/api/client"; import type { TweetData } from "@/api/types"; @@ -87,6 +87,9 @@ export function TimelineScreen({ client, }); + // Track refresh to reset PostList selection/scroll + const [refreshKey, setRefreshKey] = useState(0); + // Report post count to parent useEffect(() => { onPostCountChange?.(posts.length); @@ -105,6 +108,7 @@ export function TimelineScreen({ break; case "r": refresh(); + setRefreshKey((k) => k + 1); break; } }); @@ -156,6 +160,7 @@ export function TimelineScreen({ onLoadMore={fetchNextPage} loadingMore={isFetchingNextPage} hasMore={hasNextPage} + refreshKey={refreshKey} /> );