From e35e7eb2e4de8db082e67dfc0928e5507c5b84ab Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 22 Mar 2026 08:02:12 +0100 Subject: [PATCH] improve refresh --- src/PageManager.tsx | 50 ++++++++-- src/components/BookmarkList/index.tsx | 46 ++++++++- src/components/NormalFeed/index.tsx | 14 +-- .../Profile/ProfileFeedWithPins.tsx | 4 - src/components/Profile/index.tsx | 16 +++- src/components/Relay/index.tsx | 24 +++-- src/components/ui/RetroRefreshButton.tsx | 55 ----------- src/hooks/useFetchEvent.tsx | 33 +++++-- src/hooks/useFetchFollowings.tsx | 4 +- src/pages/primary/ExplorePage/index.tsx | 44 +++++++-- src/pages/primary/MePage/index.tsx | 38 +++++--- .../primary/NoteListPage/FollowingFeed.tsx | 21 ++-- src/pages/primary/NoteListPage/RelaysFeed.tsx | 25 +++-- src/pages/primary/NoteListPage/index.tsx | 34 ++++++- src/pages/primary/ProfilePage/index.tsx | 54 +++++++---- src/pages/primary/RelayPage/index.tsx | 39 ++++++-- src/pages/primary/RssPage/index.tsx | 23 +++-- src/pages/primary/SearchPage/index.tsx | 38 +++++--- .../primary/SettingsPrimaryPage/index.tsx | 34 +++++-- src/pages/primary/SpellsPage/index.tsx | 32 +++---- .../secondary/CacheSettingsPage/index.tsx | 20 +++- .../secondary/FollowingListPage/index.tsx | 20 +++- .../secondary/GeneralSettingsPage/index.tsx | 25 ++++- src/pages/secondary/MuteListPage/index.tsx | 22 ++++- src/pages/secondary/NotFoundPage/index.tsx | 11 ++- src/pages/secondary/NoteListPage/index.tsx | 40 ++++++-- src/pages/secondary/NotePage/index.tsx | 57 +++++++++-- .../OthersRelaySettingsPage/index.tsx | 20 +++- .../secondary/PostSettingsPage/index.tsx | 25 ++++- src/pages/secondary/ProfileListPage/index.tsx | 17 +++- src/pages/secondary/ProfilePage/index.tsx | 27 +++++- src/pages/secondary/RelayPage/index.tsx | 28 +++++- .../secondary/RelayReviewsPage/index.tsx | 27 +++++- .../secondary/RelaySettingsPage/index.tsx | 25 ++++- src/pages/secondary/RssArticlePage/index.tsx | 95 +++++++++++++++---- .../secondary/RssFeedSettingsPage/index.tsx | 64 ++++++++++++- src/pages/secondary/SearchPage/index.tsx | 40 +++++--- src/pages/secondary/SettingsPage/index.tsx | 27 +++++- src/pages/secondary/TranslationPage/index.tsx | 25 ++++- src/pages/secondary/WalletPage/index.tsx | 25 ++++- src/types/index.d.ts | 6 +- 41 files changed, 962 insertions(+), 312 deletions(-) delete mode 100644 src/components/ui/RetroRefreshButton.tsx diff --git a/src/PageManager.tsx b/src/PageManager.tsx index 88147aa0..36e48067 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -1,3 +1,4 @@ +import { RefreshButton } from '@/components/RefreshButton' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' import logger from '@/lib/logger' @@ -204,6 +205,9 @@ const PrimaryNoteViewContext = createContext<{ getNavigationCounter: () => number /** Top URL in the secondary stack (right panel), or undefined if empty. Used so settings sub-pages open in the panel instead of behind it. */ getTopSecondaryUrl: () => string | undefined + /** Primary overlay (mobile / narrow): child calls this to expose refresh for the chrome bar. */ + registerPrimaryPanelRefresh: (fn: (() => void) | null) => void + triggerPrimaryPanelRefresh: () => void } | undefined>(undefined) const NoteDrawerContext = createContext<{ @@ -599,13 +603,15 @@ function MainContentArea({ currentPrimaryPage, primaryNoteView, primaryViewType, - goBack + goBack, + onPrimaryPanelRefresh }: { primaryPages: { name: TPrimaryPageName; element: ReactNode; props?: any }[] currentPrimaryPage: TPrimaryPageName primaryNoteView: ReactNode | null primaryViewType: 'note' | 'settings' | 'settings-sub' | 'profile' | 'hashtag' | 'relay' | 'following' | 'mute' | 'others-relay-settings' | null goBack: () => void + onPrimaryPanelRefresh: () => void }) { const [, forceUpdate] = useState(0) @@ -658,7 +664,9 @@ function MainContentArea({ {getPageTitle(primaryViewType, window.location.pathname)} -
+
+ +
{primaryNoteView} @@ -717,6 +725,13 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { const [drawerNoteId, setDrawerNoteId] = useState(null) const [panelMode, setPanelMode] = useState<'single' | 'double'>(() => storage.getPanelMode()) const navigationCounterRef = useRef(0) + const primaryPanelRefreshRef = useRef<(() => void) | null>(null) + const registerPrimaryPanelRefresh = useCallback((fn: (() => void) | null) => { + primaryPanelRefreshRef.current = fn + }, []) + const triggerPrimaryPanelRefresh = useCallback(() => { + primaryPanelRefreshRef.current?.() + }, []) const savedFeedStateRef = useRef>(new Map()) const currentTabStateRef = useRef>(new Map()) // Track current tab state for each page const savedPrimaryPagePropsRef = useRef(undefined) @@ -1571,7 +1586,17 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { > - navigationCounterRef.current, getTopSecondaryUrl: () => secondaryStack.length > 0 ? secondaryStack[secondaryStack.length - 1].url : undefined }}> + navigationCounterRef.current, + getTopSecondaryUrl: () => + secondaryStack.length > 0 ? secondaryStack[secondaryStack.length - 1].url : undefined, + registerPrimaryPanelRefresh, + triggerPrimaryPanelRefresh + }} + > {primaryNoteView ? ( // Show primary note view with back button on mobile @@ -1582,9 +1607,9 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
-
+
+
{primaryNoteView} @@ -1700,7 +1726,17 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { > - navigationCounterRef.current, getTopSecondaryUrl: () => secondaryStack.length > 0 ? secondaryStack[secondaryStack.length - 1].url : undefined }}> + navigationCounterRef.current, + getTopSecondaryUrl: () => + secondaryStack.length > 0 ? secondaryStack[secondaryStack.length - 1].url : undefined, + registerPrimaryPanelRefresh, + triggerPrimaryPanelRefresh + }} + >
{/* Right: secondary stack — max width so left pane keeps space on small desktops */} @@ -1763,6 +1800,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { primaryNoteView={primaryNoteView} primaryViewType={primaryViewType} goBack={goBack} + onPrimaryPanelRefresh={triggerPrimaryPanelRefresh} />
) diff --git a/src/components/BookmarkList/index.tsx b/src/components/BookmarkList/index.tsx index 3aa2ee74..c5bc66d8 100644 --- a/src/components/BookmarkList/index.tsx +++ b/src/components/BookmarkList/index.tsx @@ -1,15 +1,20 @@ import { useFetchEvent } from '@/hooks' +import { PROFILE_FETCH_RELAY_URLS } from '@/constants' +import { getLatestEvent } from '@/lib/event' import { generateBech32IdFromATag, generateBech32IdFromETag } from '@/lib/tag' +import { normalizeUrl } from '@/lib/url' import { useNostr } from '@/providers/NostrProvider' -import { useEffect, useMemo, useRef, useState } from 'react' +import { queryService } from '@/services/client.service' +import { kinds } from 'nostr-tools' +import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' const SHOW_COUNT = 10 -export default function BookmarkList() { +const BookmarkList = forwardRef(function BookmarkList(_, ref) { const { t } = useTranslation() - const { bookmarkListEvent } = useNostr() + const { bookmarkListEvent, pubkey, relayList, updateBookmarkListEvent } = useNostr() const eventIds = useMemo(() => { if (!bookmarkListEvent) return [] @@ -28,6 +33,36 @@ export default function BookmarkList() { const [showCount, setShowCount] = useState(SHOW_COUNT) const bottomRef = useRef(null) + useImperativeHandle( + ref, + () => ({ + refresh: async () => { + if (!pubkey) return + const urls = Array.from( + new Set( + [ + ...PROFILE_FETCH_RELAY_URLS.map((u) => normalizeUrl(u) || u), + ...(relayList?.write ?? []).map((u) => normalizeUrl(u) || u) + ].filter(Boolean) + ) + ).slice(0, 12) + if (urls.length === 0) return + try { + const events = await queryService.fetchEvents(urls, { + kinds: [kinds.BookmarkList], + authors: [pubkey], + limit: 5 + }) + const latest = getLatestEvent(events) + if (latest) await updateBookmarkListEvent(latest) + } catch { + /* ignore */ + } + } + }), + [pubkey, relayList, updateBookmarkListEvent] + ) + useEffect(() => { const options = { root: null, @@ -85,7 +120,10 @@ export default function BookmarkList() { )}
) -} +}) + +BookmarkList.displayName = 'BookmarkList' +export default BookmarkList function BookmarkedNote({ eventId }: { eventId: string }) { const { event, isFetching } = useFetchEvent(eventId) diff --git a/src/components/NormalFeed/index.tsx b/src/components/NormalFeed/index.tsx index d1b3af43..2cd3858c 100644 --- a/src/components/NormalFeed/index.tsx +++ b/src/components/NormalFeed/index.tsx @@ -6,7 +6,6 @@ import storage from '@/services/local-storage.service' import { TFeedSubRequest, TNoteListMode } from '@/types' import { forwardRef, useLayoutEffect, useMemo, useRef, useState } from 'react' import KindFilter from '../KindFilter' -import { RefreshButton } from '../RefreshButton' const NormalFeed = forwardRef handleListModeChange(tab)} - options={ - <> - { - if (noteListRef && typeof noteListRef !== 'function') { - noteListRef.current?.refresh() - } - }} - /> - - - } + options={} /> ) diff --git a/src/components/Profile/ProfileFeedWithPins.tsx b/src/components/Profile/ProfileFeedWithPins.tsx index 5de8a3e6..c9eabe44 100644 --- a/src/components/Profile/ProfileFeedWithPins.tsx +++ b/src/components/Profile/ProfileFeedWithPins.tsx @@ -1,6 +1,5 @@ import NoteCard from '@/components/NoteCard' import ProfileSearchBar from '@/components/ui/ProfileSearchBar' -import RetroRefreshButton from '@/components/ui/RetroRefreshButton' import { Skeleton } from '@/components/ui/skeleton' import { ExtendedKind, PROFILE_FEED_KINDS } from '@/constants' import { isReplyNoteEvent } from '@/lib/event' @@ -159,7 +158,6 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string placeholder={t('Search posts...')} className="w-64 max-w-full" /> -
{Array.from({ length: 4 }).map((_, i) => ( @@ -179,7 +177,6 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string placeholder={t('Search posts...')} className="w-64 max-w-full" /> -
{searchQuery.trim() ? t('No posts match your search') : t('No posts found')} @@ -196,7 +193,6 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string placeholder={t('Search posts...')} className="w-64 max-w-full" /> -
{isRefreshing && (
🔄 {t('Refreshing posts...')}
diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index 3e543f73..4ff4f4cb 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -28,7 +28,7 @@ import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { Copy, Ellipsis, Calendar, MapPin, Pencil, SatelliteDish, Code, Gift, Link } from 'lucide-react' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState, type Ref } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import logger from '@/lib/logger' @@ -153,10 +153,20 @@ function mergePaymentMethods( return out } -export default function Profile({ id }: { id?: string }) { +export default function Profile({ + id, + feedRef +}: { + id?: string + /** When set, exposes {@link ProfileFeedWithPins} `refresh` for titlebars / parent pages. */ + feedRef?: Ref<{ refresh: () => void }> +}) { const { t } = useTranslation() const { push } = useSecondaryPage() const { navigate: navigatePrimary } = usePrimaryPage() + const internalFeedRef = useRef<{ refresh: () => void }>(null) + const profileFeedRef = feedRef ?? internalFeedRef + const { profile, isFetching } = useFetchProfile(id) const { pubkey: accountPubkey } = useNostr() const [paymentInfo, setPaymentInfo] = useState | null>(null) @@ -562,7 +572,7 @@ export default function Profile({ id }: { id?: string }) { - + {openPublicMessageTo && ( (function Relay( + { url, className }, + ref +) { const { t } = useTranslation() const { addRelayUrls, removeRelayUrls } = useCurrentRelays() const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url]) const { relayInfo } = useFetchRelayInfo(normalizedUrl) const [searchInput, setSearchInput] = useState('') const [debouncedInput, setDebouncedInput] = useState(searchInput) - const noteListRef = useRef(null) + const internalNoteListRef = useRef(null) + const noteListRef = ref ?? internalNoteListRef useEffect(() => { if (normalizedUrl) { @@ -44,8 +48,9 @@ export default function Relay({ url, className }: { url?: string; className?: st const handleRelayRefresh = (event: CustomEvent) => { const { relayUrl } = event.detail if (normalizeUrl(relayUrl) === normalizedUrl) { - // Trigger a refresh of the note list - noteListRef.current?.refresh() + if (noteListRef && typeof noteListRef !== 'function') { + noteListRef.current?.refresh() + } } } @@ -54,7 +59,7 @@ export default function Relay({ url, className }: { url?: string; className?: st return () => { window.removeEventListener('relay-refresh-needed', handleRelayRefresh as EventListener) } - }, [normalizedUrl]) + }, [normalizedUrl, noteListRef]) if (!normalizedUrl) { return @@ -80,4 +85,7 @@ export default function Relay({ url, className }: { url?: string; className?: st /> ) -} +}) + +Relay.displayName = 'Relay' +export default Relay diff --git a/src/components/ui/RetroRefreshButton.tsx b/src/components/ui/RetroRefreshButton.tsx deleted file mode 100644 index 668e86cd..00000000 --- a/src/components/ui/RetroRefreshButton.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Button } from '@/components/ui/button' -import { RefreshCw } from 'lucide-react' -import { cn } from '@/lib/utils' - -interface RetroRefreshButtonProps { - onClick: () => void - isLoading?: boolean - className?: string - size?: 'sm' | 'md' | 'lg' -} - -export default function RetroRefreshButton({ - onClick, - isLoading = false, - className, - size = 'md' -}: RetroRefreshButtonProps) { - const sizeClasses = { - sm: 'h-8 w-8 p-1', - md: 'h-10 w-10 p-2', - lg: 'h-12 w-12 p-3' - } - - const iconSizes = { - sm: 'h-4 w-4', - md: 'h-5 w-5', - lg: 'h-6 w-6' - } - - return ( - - ) -} diff --git a/src/hooks/useFetchEvent.tsx b/src/hooks/useFetchEvent.tsx index 82a32564..5758d961 100644 --- a/src/hooks/useFetchEvent.tsx +++ b/src/hooks/useFetchEvent.tsx @@ -3,7 +3,7 @@ import { useReply } from '@/providers/ReplyProvider' import { eventService } from '@/services/client.service' import { navigationEventStore } from '@/services/navigation-event-store' import { Event } from 'nostr-tools' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' export function useFetchEvent(eventId?: string, initialEvent?: Event) { const { isEventDeleted } = useDeletedEvent() @@ -11,6 +11,11 @@ export function useFetchEvent(eventId?: string, initialEvent?: Event) { const [error, setError] = useState(null) const [event, setEvent] = useState(initialEvent) const [isFetching, setIsFetching] = useState(!initialEvent) + const [refetchToken, setRefetchToken] = useState(0) + + const refetch = useCallback(() => { + setRefetchToken((n) => n + 1) + }, []) useEffect(() => { if (!eventId) { @@ -19,8 +24,14 @@ export function useFetchEvent(eventId?: string, initialEvent?: Event) { return } + const skipShortcuts = refetchToken > 0 + // If we have an initial event that matches the eventId, use it and skip fetching - if (initialEvent && (initialEvent.id === eventId || eventId.includes(initialEvent.id))) { + if ( + !skipShortcuts && + initialEvent && + (initialEvent.id === eventId || eventId.includes(initialEvent.id)) + ) { if (!isEventDeleted(initialEvent)) { setEvent(initialEvent) addReplies([initialEvent]) @@ -30,12 +41,14 @@ export function useFetchEvent(eventId?: string, initialEvent?: Event) { } // Check navigation event store first (events passed through navigation) - const navigationEvent = navigationEventStore.getEvent(eventId) - if (navigationEvent && !isEventDeleted(navigationEvent)) { - setEvent(navigationEvent) - addReplies([navigationEvent]) - setIsFetching(false) - return + if (!skipShortcuts) { + const navigationEvent = navigationEventStore.getEvent(eventId) + if (navigationEvent && !isEventDeleted(navigationEvent)) { + setEvent(navigationEvent) + addReplies([navigationEvent]) + setIsFetching(false) + return + } } setIsFetching(true) @@ -56,7 +69,7 @@ export function useFetchEvent(eventId?: string, initialEvent?: Event) { } fetchEvent() - }, [eventId, initialEvent, isEventDeleted, addReplies]) + }, [eventId, initialEvent, isEventDeleted, addReplies, refetchToken]) useEffect(() => { if (event && isEventDeleted(event)) { @@ -64,5 +77,5 @@ export function useFetchEvent(eventId?: string, initialEvent?: Event) { } }, [isEventDeleted, event]) - return { isFetching, error, event } + return { isFetching, error, event, refetch } } diff --git a/src/hooks/useFetchFollowings.tsx b/src/hooks/useFetchFollowings.tsx index 62cd4c9b..6c2cd0a8 100644 --- a/src/hooks/useFetchFollowings.tsx +++ b/src/hooks/useFetchFollowings.tsx @@ -4,7 +4,7 @@ import { kinds } from 'nostr-tools' import { Event } from 'nostr-tools' import { useEffect, useState } from 'react' -export function useFetchFollowings(pubkey?: string | null) { +export function useFetchFollowings(pubkey?: string | null, refreshNonce = 0) { const [followListEvent, setFollowListEvent] = useState(null) const [followings, setFollowings] = useState([]) const [isFetching, setIsFetching] = useState(true) @@ -26,7 +26,7 @@ export function useFetchFollowings(pubkey?: string | null) { } init() - }, [pubkey]) + }, [pubkey, refreshNonce]) return { followings, followListEvent, isFetching } } diff --git a/src/pages/primary/ExplorePage/index.tsx b/src/pages/primary/ExplorePage/index.tsx index e1a31d2a..edd5d5f5 100644 --- a/src/pages/primary/ExplorePage/index.tsx +++ b/src/pages/primary/ExplorePage/index.tsx @@ -9,11 +9,13 @@ import { Input } from '@/components/ui/input' import { toRelay } from '@/lib/link' import { cn } from '@/lib/utils' import { isWebsocketUrl, normalizeUrl, simplifyUrl } from '@/lib/url' +import { RefreshButton } from '@/components/RefreshButton' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' import { useSmartRelayNavigation } from '@/PageManager' import nip66Service from '@/services/nip66.service' +import { TPageRef } from '@/types' import { ArrowRight, Compass, Plus } from 'lucide-react' -import { forwardRef, FormEvent, useEffect, useMemo, useRef, useState } from 'react' +import { forwardRef, FormEvent, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -65,9 +67,22 @@ function normalizeHomeTab(restored: string): TExploreTabs { return 'explore' } -const ExplorePage = forwardRef((_, ref) => { +const ExplorePage = forwardRef((_, ref) => { const { t } = useTranslation() const [tab, setTab] = useState('explore') + const layoutRef = useRef(null) + const [contentRefreshKey, setContentRefreshKey] = useState(0) + + const bumpExploreContent = () => setContentRefreshKey((k) => k + 1) + + useImperativeHandle( + ref, + () => ({ + scrollToTop: (behavior?: ScrollBehavior) => layoutRef.current?.scrollToTop(behavior), + refresh: bumpExploreContent + }), + [] + ) // Listen for tab restoration from PageManager useEffect(() => { @@ -82,9 +97,9 @@ const ExplorePage = forwardRef((_, ref) => { return ( } + titlebar={} subHeader={ { {tab === 'explore' && ( - <> +
- +
+ )} + {tab === 'reviews' && ( +
+ +
+ )} + {tab === 'following' && ( +
+ +
)} - {tab === 'reviews' && } - {tab === 'following' && }
) @@ -125,7 +148,7 @@ const ExplorePage = forwardRef((_, ref) => { ExplorePage.displayName = 'ExplorePage' export default ExplorePage -function ExplorePageTitlebar() { +function ExplorePageTitlebar({ onRefresh }: { onRefresh: () => void }) { const { t } = useTranslation() return ( @@ -134,6 +157,8 @@ function ExplorePageTitlebar() {
{t('Explore')}
+
+ +
) } diff --git a/src/pages/primary/MePage/index.tsx b/src/pages/primary/MePage/index.tsx index 82b2af35..e133f672 100644 --- a/src/pages/primary/MePage/index.tsx +++ b/src/pages/primary/MePage/index.tsx @@ -6,7 +6,8 @@ import NpubQrCode from '@/components/NpubQrCode' import { Separator } from '@/components/ui/separator' import { SimpleUserAvatar } from '@/components/UserAvatar' import { SimpleUsername } from '@/components/Username' -import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' +import { RefreshButton } from '@/components/RefreshButton' +import PrimaryPageLayout, { type TPrimaryPageLayoutRef } from '@/layouts/PrimaryPageLayout' import { toProfile, toRelaySettings, toWallet } from '@/lib/link' import { cn } from '@/lib/utils' import { useSecondaryPage } from '@/PageManager' @@ -19,25 +20,39 @@ import { UserRound, Wallet } from 'lucide-react' -import { forwardRef, HTMLProps, useState, type KeyboardEvent, type MouseEvent } from 'react' +import { TPageRef } from '@/types' +import { forwardRef, HTMLProps, useImperativeHandle, useRef, useState, type KeyboardEvent, type MouseEvent } from 'react' import { useTranslation } from 'react-i18next' -const MePage = forwardRef((_, ref) => { +const MePage = forwardRef((_, ref) => { const { t } = useTranslation() const { push } = useSecondaryPage() const { pubkey } = useNostr() const [loginDialogOpen, setLoginDialogOpen] = useState(false) const [logoutDialogOpen, setLogoutDialogOpen] = useState(false) + const layoutRef = useRef(null) + const [contentKey, setContentKey] = useState(0) + + const bumpMe = () => setContentKey((k) => k + 1) + + useImperativeHandle( + ref, + () => ({ + scrollToTop: (behavior?: ScrollBehavior) => layoutRef.current?.scrollToTop(behavior), + refresh: bumpMe + }), + [] + ) if (!pubkey) { return ( } + titlebar={} hideTitlebarBottomBorder > -
+
@@ -46,12 +61,12 @@ const MePage = forwardRef((_, ref) => { return ( } + titlebar={} hideTitlebarBottomBorder > -
+
@@ -100,11 +115,12 @@ const MePage = forwardRef((_, ref) => { MePage.displayName = 'MePage' export default MePage -function MePageTitlebar() { +function MePageTitlebar({ onRefresh }: { onRefresh: () => void }) { const { t } = useTranslation() return ( -
+
{t('YouTabName')}
+
) } diff --git a/src/pages/primary/NoteListPage/FollowingFeed.tsx b/src/pages/primary/NoteListPage/FollowingFeed.tsx index 3c26da79..4ee0168a 100644 --- a/src/pages/primary/NoteListPage/FollowingFeed.tsx +++ b/src/pages/primary/NoteListPage/FollowingFeed.tsx @@ -1,16 +1,18 @@ import NormalFeed from '@/components/NormalFeed' +import type { TNoteListRef } from '@/components/NoteList' import { useFeed } from '@/providers/FeedProvider' import { useNostr } from '@/providers/NostrProvider' import client from '@/services/client.service' import { TFeedSubRequest } from '@/types' import type { ReactNode } from 'react' -import { useEffect, useState } from 'react' +import { forwardRef, useEffect, useState } from 'react' -export default function FollowingFeed({ - setSubHeader -}: { - setSubHeader?: (node: ReactNode) => void -}) { +const FollowingFeed = forwardRef< + TNoteListRef, + { + setSubHeader?: (node: ReactNode) => void + } +>(function FollowingFeed({ setSubHeader }, ref) { const { pubkey } = useNostr() const { feedInfo } = useFeed() const [subRequests, setSubRequests] = useState([]) @@ -29,5 +31,8 @@ export default function FollowingFeed({ init() }, [feedInfo.feedType, pubkey]) - return -} + return +}) + +FollowingFeed.displayName = 'FollowingFeed' +export default FollowingFeed diff --git a/src/pages/primary/NoteListPage/RelaysFeed.tsx b/src/pages/primary/NoteListPage/RelaysFeed.tsx index b4f33bbe..3637ee78 100644 --- a/src/pages/primary/NoteListPage/RelaysFeed.tsx +++ b/src/pages/primary/NoteListPage/RelaysFeed.tsx @@ -1,19 +1,20 @@ import NormalFeed from '@/components/NormalFeed' +import type { TNoteListRef } from '@/components/NoteList' import { checkAlgoRelay } from '@/lib/relay' import { useFeed } from '@/providers/FeedProvider' import { useKindFilter } from '@/providers/KindFilterProvider' import relayInfoService from '@/services/relay-info.service' import { kinds } from 'nostr-tools' -import React, { useEffect, useMemo, useState, useRef } from 'react' +import React, { forwardRef, useEffect, useMemo, useState, useRef } from 'react' -export default function RelaysFeed({ - setSubHeader, - kindsOverride -}: { - setSubHeader?: (node: React.ReactNode) => void - /** When set, subscription kinds (fixed list); otherwise uses KindFilterProvider. */ - kindsOverride?: number[] -}) { +const RelaysFeed = forwardRef< + TNoteListRef, + { + setSubHeader?: (node: React.ReactNode) => void + /** When set, subscription kinds (fixed list); otherwise uses KindFilterProvider. */ + kindsOverride?: number[] + } +>(function RelaysFeed({ setSubHeader, kindsOverride }, ref) { const { feedInfo, relayUrls } = useFeed() const { showKinds } = useKindFilter() const [areAlgoRelays, setAreAlgoRelays] = useState(false) @@ -92,10 +93,14 @@ export default function RelaysFeed({ return ( ) -} +}) + +RelaysFeed.displayName = 'RelaysFeed' +export default RelaysFeed diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx index ebfe8c60..25426abf 100644 --- a/src/pages/primary/NoteListPage/index.tsx +++ b/src/pages/primary/NoteListPage/index.tsx @@ -1,5 +1,6 @@ import BookmarkList from '@/components/BookmarkList' import RelayInfo from '@/components/RelayInfo' +import { RefreshButton } from '@/components/RefreshButton' import VersionUpdateBanner from '@/components/VersionUpdateBanner' import { Button } from '@/components/ui/button' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' @@ -7,6 +8,7 @@ import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { useFeed } from '@/providers/FeedProvider' import { useNostr } from '@/providers/NostrProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' +import type { TNoteListRef } from '@/components/NoteList' import { TPageRef } from '@/types' import { Compass, Info } from 'lucide-react' import React, { @@ -25,15 +27,33 @@ import FollowingFeed from './FollowingFeed' import RelaysFeed from './RelaysFeed' import { usePrimaryNoteView, usePrimaryPage } from '@/PageManager' -const NoteListPage = forwardRef((_, ref) => { +const NoteListPage = forwardRef((_, ref) => { const { t } = useTranslation() const { addRelayUrls, removeRelayUrls } = useCurrentRelays() const layoutRef = useRef(null) + const feedRef = useRef(null) + const bookmarkRef = useRef<{ refresh: () => void }>(null) const { pubkey, checkLogin } = useNostr() const { feedInfo, relayUrls, isReady } = useFeed() const [showRelayDetails, setShowRelayDetails] = useState(false) const [homeSubHeader, setHomeSubHeader] = useState(null) - useImperativeHandle(ref, () => layoutRef.current) + + const runFeedRefresh = useCallback(() => { + if (feedInfo.feedType === 'bookmarks') { + void bookmarkRef.current?.refresh() + } else { + feedRef.current?.refresh() + } + }, [feedInfo.feedType]) + + useImperativeHandle( + ref, + () => ({ + scrollToTop: (behavior?: ScrollBehavior) => layoutRef.current?.scrollToTop(behavior), + refresh: runFeedRefresh + }), + [runFeedRefresh] + ) const setHomeSubHeaderStable = useCallback((node: React.ReactNode) => { setHomeSubHeader(node) @@ -82,17 +102,17 @@ const NoteListPage = forwardRef((_, ref) => {
) } else { - content = + content = } } else if (feedInfo.feedType === 'following') { - content = + content = } else { content = ( <> {showRelayDetails && feedInfo.feedType === 'relay' && !!feedInfo.id && ( )} - + ) } @@ -104,6 +124,7 @@ const NoteListPage = forwardRef((_, ref) => { titlebar={ + onFeedRefresh: () => void showRelayDetails?: boolean setShowRelayDetails?: Dispatch> }) { @@ -176,6 +199,7 @@ function NoteListPageTitlebar({
)}
+ {setShowRelayDetails && (
- {pubkey ? ( - - ) : null} +
+ + {pubkey ? ( + + ) : null} +
) } diff --git a/src/pages/primary/RelayPage/index.tsx b/src/pages/primary/RelayPage/index.tsx index cbc9ce07..5081d7a7 100644 --- a/src/pages/primary/RelayPage/index.tsx +++ b/src/pages/primary/RelayPage/index.tsx @@ -1,21 +1,39 @@ +import type { TNoteListRef } from '@/components/NoteList' +import { RefreshButton } from '@/components/RefreshButton' import Relay from '@/components/Relay' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' +import { TPageRef } from '@/types' import { normalizeUrl, simplifyUrl } from '@/lib/url' import { Server } from 'lucide-react' -import { forwardRef, useMemo } from 'react' +import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react' -const RelayPage = forwardRef(({ url }: { url?: string }, ref) => { +const RelayPage = forwardRef(({ url }, ref) => { const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url]) + const layoutRef = useRef(null) + const feedRef = useRef(null) + + const runRefresh = useCallback(() => { + feedRef.current?.refresh() + }, []) + + useImperativeHandle( + ref, + () => ({ + scrollToTop: (behavior?: ScrollBehavior) => layoutRef.current?.scrollToTop(behavior), + refresh: runRefresh + }), + [runRefresh] + ) return ( } + titlebar={} displayScrollToTopButton - ref={ref} + ref={layoutRef} >
- +
) @@ -23,11 +41,14 @@ const RelayPage = forwardRef(({ url }: { url?: string }, ref) => { RelayPage.displayName = 'RelayPage' export default RelayPage -function RelayPageTitlebar({ url }: { url?: string }) { +function RelayPageTitlebar({ url, onRefresh }: { url?: string; onRefresh: () => void }) { return ( -
- -
{simplifyUrl(url ?? '')}
+
+
+ +
{simplifyUrl(url ?? '')}
+
+
) } diff --git a/src/pages/primary/RssPage/index.tsx b/src/pages/primary/RssPage/index.tsx index c34e7e08..e8b9f0b2 100644 --- a/src/pages/primary/RssPage/index.tsx +++ b/src/pages/primary/RssPage/index.tsx @@ -1,21 +1,23 @@ import RssFeedList from '@/components/RssFeedList' import { RefreshButton } from '@/components/RefreshButton' -import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' +import PrimaryPageLayout, { type TPrimaryPageLayoutRef } from '@/layouts/PrimaryPageLayout' import { Button } from '@/components/ui/button' import { DEFAULT_RSS_FEEDS } from '@/constants' import logger from '@/lib/logger' import { useNostr } from '@/providers/NostrProvider' import rssFeedService from '@/services/rss-feed.service' import { Rss, Search } from 'lucide-react' -import { forwardRef, useState } from 'react' +import { TPageRef } from '@/types' +import { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -const RssPage = forwardRef((_, ref) => { +const RssPage = forwardRef((_, ref) => { const { t } = useTranslation() const { pubkey, rssFeedListEvent } = useNostr() const [rssRefreshKey, setRssRefreshKey] = useState(0) + const layoutRef = useRef(null) - const handleRefresh = () => { + const handleRefresh = useCallback(() => { let feedUrls: string[] = [] if (pubkey && rssFeedListEvent) { try { @@ -40,11 +42,20 @@ const RssPage = forwardRef((_, ref) => { ) } setRssRefreshKey((k) => k + 1) - } + }, [pubkey, rssFeedListEvent]) + + useImperativeHandle( + ref, + () => ({ + scrollToTop: (behavior?: ScrollBehavior) => layoutRef.current?.scrollToTop(behavior), + refresh: handleRefresh + }), + [handleRefresh] + ) return ( diff --git a/src/pages/primary/SearchPage/index.tsx b/src/pages/primary/SearchPage/index.tsx index 3dc4fea2..9c55b9c0 100644 --- a/src/pages/primary/SearchPage/index.tsx +++ b/src/pages/primary/SearchPage/index.tsx @@ -1,27 +1,32 @@ import LatestFromFollowsSection from '@/components/LatestFromFollowsSection' +import { RefreshButton } from '@/components/RefreshButton' import SearchBar, { TSearchBarRef } from '@/components/SearchBar' import SearchResult from '@/components/SearchResult' import PrimaryPageLayout, { TPrimaryPageLayoutRef } from '@/layouts/PrimaryPageLayout' import { usePrimaryPage } from '@/PageManager' -import { TSearchParams } from '@/types' +import { TPageRef, TSearchParams } from '@/types' import { BookOpen } from 'lucide-react' import { Button } from '@/components/ui/button' -import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' -const SearchPage = forwardRef((_, ref) => { +const SearchPage = forwardRef((_, ref) => { const { current, display } = usePrimaryPage() const [input, setInput] = useState('') const [searchParams, setSearchParams] = useState(null) + const [resultRefreshKey, setResultRefreshKey] = useState(0) const isActive = useMemo(() => current === 'search' && display, [current, display]) const searchBarRef = useRef(null) const layoutRef = useRef(null) + const bumpResults = useCallback(() => setResultRefreshKey((k) => k + 1), []) + useImperativeHandle( ref, () => ({ - scrollToTop: (behavior: ScrollBehavior = 'smooth') => layoutRef.current?.scrollToTop(behavior) + scrollToTop: (behavior: ScrollBehavior = 'smooth') => layoutRef.current?.scrollToTop(behavior), + refresh: bumpResults }), - [] + [bumpResults] ) useEffect(() => { @@ -46,7 +51,10 @@ const SearchPage = forwardRef((_, ref) => { displayScrollToTopButton >
-
Search Nostr
+
+
Search Nostr
+ +
@@ -69,14 +77,16 @@ const SearchPage = forwardRef((_, ref) => {
- {searchParams ? ( - - ) : ( -
- - -
- )} +
+ {searchParams ? ( + + ) : ( +
+ + +
+ )} +
) diff --git a/src/pages/primary/SettingsPrimaryPage/index.tsx b/src/pages/primary/SettingsPrimaryPage/index.tsx index 82defeeb..ae4baace 100644 --- a/src/pages/primary/SettingsPrimaryPage/index.tsx +++ b/src/pages/primary/SettingsPrimaryPage/index.tsx @@ -1,25 +1,43 @@ +import { RefreshButton } from '@/components/RefreshButton' import SettingsMenuBody from '@/components/Settings/SettingsMenuBody' -import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' +import PrimaryPageLayout, { type TPrimaryPageLayoutRef } from '@/layouts/PrimaryPageLayout' +import { TPageRef } from '@/types' import { Settings } from 'lucide-react' -import { forwardRef } from 'react' +import { forwardRef, useImperativeHandle, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -const SettingsPrimaryPage = forwardRef((_, ref) => { +const SettingsPrimaryPage = forwardRef((_, ref) => { const { t } = useTranslation() + const layoutRef = useRef(null) + const [menuKey, setMenuKey] = useState(0) + + const bumpMenu = () => setMenuKey((k) => k + 1) + + useImperativeHandle( + ref, + () => ({ + scrollToTop: (behavior?: ScrollBehavior) => layoutRef.current?.scrollToTop(behavior), + refresh: bumpMenu + }), + [] + ) return ( - -
{t('Settings')}
+
+
+ +
{t('Settings')}
+
+
} displayScrollToTopButton > -
+
diff --git a/src/pages/primary/SpellsPage/index.tsx b/src/pages/primary/SpellsPage/index.tsx index e5822835..275ac507 100644 --- a/src/pages/primary/SpellsPage/index.tsx +++ b/src/pages/primary/SpellsPage/index.tsx @@ -1,5 +1,6 @@ import HideUntrustedContentButton from '@/components/HideUntrustedContentButton' import NoteList, { type TNoteListRef } from '@/components/NoteList' +import { RefreshButton } from '@/components/RefreshButton' import { Button } from '@/components/ui/button' import { Dialog, @@ -18,7 +19,7 @@ import { Separator } from '@/components/ui/separator' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer' import UserAvatar from '@/components/UserAvatar' import Username from '@/components/Username' -import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' +import PrimaryPageLayout, { type TPrimaryPageLayoutRef } from '@/layouts/PrimaryPageLayout' import { usePrimaryPage } from '@/PageManager' import logger from '@/lib/logger' import { showPublishingError } from '@/lib/publishing-feedback' @@ -70,7 +71,6 @@ import { MoreVertical, Pencil, Plus, - RefreshCw, Star, Trash2, Users, @@ -78,7 +78,7 @@ import { } from 'lucide-react' import type { Event } from 'nostr-tools' import { verifyEvent } from 'nostr-tools' -import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import CreateSpellDialog from './CreateSpellDialog' import { @@ -275,7 +275,7 @@ const SpellsPage = forwardRef(function SpellsPage( /** Bumps spell catalog relay re-sync when the user taps refresh in the titlebar. */ const [spellCatalogManualRefreshKey, setSpellCatalogManualRefreshKey] = useState(0) const spellFeedListRef = useRef(null) - const [titlebarRefreshSpin, setTitlebarRefreshSpin] = useState(false) + const layoutRef = useRef(null) const [spellPickerOpen, setSpellPickerOpen] = useState(false) /** Monotonic token + wall time for spell-feed latency instrumentation (picker → first rows). */ @@ -332,13 +332,20 @@ const SpellsPage = forwardRef(function SpellsPage( }, []) const refreshSpellsFeedAndCatalog = useCallback(() => { - setTitlebarRefreshSpin(true) - window.setTimeout(() => setTitlebarRefreshSpin(false), 600) void loadSpells() if (pubkey) setSpellCatalogManualRefreshKey((k) => k + 1) spellFeedListRef.current?.refresh() }, [loadSpells, pubkey]) + useImperativeHandle( + ref, + () => ({ + scrollToTop: (behavior?: ScrollBehavior) => layoutRef.current?.scrollToTop(behavior), + refresh: refreshSpellsFeedAndCatalog + }), + [refreshSpellsFeedAndCatalog] + ) + /** * Fingerprint by value — `relayList` from NostrProvider often gets a new object ref each render. * Using `[relayList]` in useMemo deps was invalidating every tick → new subRequests → browse-relay @@ -1007,22 +1014,13 @@ const SpellsPage = forwardRef(function SpellsPage( return (
{t('Spells')}
- +