From 616b6bbc929adf4a4555fd11276f58bd156ef746 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 25 May 2026 19:24:24 +0200 Subject: [PATCH] bug-fixes --- src/PageManager.tsx | 51 +++---- .../ActiveRelaysDropdownSection.tsx | 58 ++++++++ .../ActiveRelaysTitlebarButton.tsx | 139 ------------------ src/components/DrawerMenuItem/index.tsx | 12 +- .../RelayPulseActiveNpubsSheet.tsx | 13 +- .../FavoriteRelaysActiveStrip/index.tsx | 98 ++---------- .../relay-pulse-relative-time.ts | 26 ++++ src/components/HelpAndAccountMenu.tsx | 54 +++++-- src/components/NoteOptions/DesktopMenu.tsx | 12 +- src/components/NoteOptions/MobileMenu.tsx | 66 ++++++--- src/components/NoteOptions/index.tsx | 25 +++- src/components/NoteStats/RepostButton.tsx | 19 ++- src/components/NoteStats/SeenOnButton.tsx | 14 +- src/components/NoteStats/index.tsx | 28 ++-- src/hooks/useSeenOnRelays.ts | 8 +- src/layouts/PrimaryPageLayout/index.tsx | 28 +--- src/layouts/SecondaryPageLayout/index.tsx | 6 +- src/lib/home-feed-relays.ts | 7 +- src/lib/nostr-land-relay-eligibility.ts | 5 + src/pages/primary/NoteListPage/index.tsx | 5 - src/services/note-stats.service.ts | 23 ++- 21 files changed, 338 insertions(+), 359 deletions(-) create mode 100644 src/components/ConnectedRelays/ActiveRelaysDropdownSection.tsx delete mode 100644 src/components/ConnectedRelays/ActiveRelaysTitlebarButton.tsx create mode 100644 src/components/FavoriteRelaysActiveStrip/relay-pulse-relative-time.ts diff --git a/src/PageManager.tsx b/src/PageManager.tsx index d697fb73..a8010920 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -502,10 +502,8 @@ export function useSmartNoteNavigation() { const contextualUrl = buildNoteUrl(noteId, currentPrimaryPage) if (isSmallScreen) { - // Mobile: always push to secondary stack AND update drawer - // This ensures back button works when clicking embedded events + // Mobile: full-screen secondary stack (no sheet drawer — overlay hid the stack and showed black). pushSecondaryPage(contextualUrl) - openDrawer(noteId, event) } else { // Desktop: check panel mode const currentPanelMode = storage.getPanelMode() @@ -567,7 +565,6 @@ export function useSmartNoteNavigationOptional() { const contextualUrl = buildNoteUrl(noteId, currentPrimaryPage) if (isSmallScreen) { push(contextualUrl) - openDrawer(noteId, event) } else { const currentPanelMode = storage.getPanelMode() if (currentPanelMode === 'single') { @@ -1217,12 +1214,14 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { // Drawer handlers const [drawerInitialEvent, setDrawerInitialEvent] = useState(null) const openDrawer = useCallback((noteId: string, initialEvent?: Event) => { + // Mobile uses the full-screen secondary stack; the sheet drawer only applies to desktop single-pane. + if (isSmallScreen || panelMode !== 'single') return noteStatsService.setBackgroundStatsPaused(true) client.interruptBackgroundQueries() setDrawerNoteId(noteId) setDrawerInitialEvent(initialEvent ?? null) setDrawerOpen(true) - }, []) + }, [isSmallScreen, panelMode]) const closeDrawer = useCallback(() => { if (!drawerOpen) return // Already closed @@ -1361,9 +1360,11 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { // Open drawer immediately, then load background page asynchronously // This prevents the background page loading from blocking the drawer if (isSmallScreen || panelMode === 'single') { - // Seed stack so in-drawer navigation (e.g. quotes → back) can pop to this note + // Seed stack so in-note navigation (e.g. quotes → back) can pop to this note pushNoteUrlOnStack(buildNoteUrl(noteId, resolved.name)) - openDrawer(noteId) + if (!isSmallScreen) { + openDrawer(noteId) + } setTimeout(() => { setCurrentPrimaryPage(resolved.name) @@ -1384,7 +1385,9 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { if (isSmallScreen || panelMode === 'single') { pushNoteUrlOnStack(contextualUrl) - openDrawer(noteId) + if (!isSmallScreen) { + openDrawer(noteId) + } return } else { pushNoteUrlOnStack(contextualUrl) @@ -1672,7 +1675,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { window.location.pathname + window.location.search + window.location.hash if (locUrl !== '/' && locUrl !== '') { const synced = syncSecondaryStackWhenPopStateStateIsNull(pre, locUrl) - if ((isSmallScreen || panelMode === 'single') && drawerOpen && drawerNoteId && synced.length > 0) { + if ((panelMode === 'single' && !isSmallScreen) && drawerOpen && drawerNoteId && synced.length > 0) { const topItemUrl = synced[synced.length - 1]?.url if (topItemUrl) { const topNoteUrlMatch = @@ -1787,8 +1790,9 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { const noteId = noteUrlMatch[noteUrlMatch.length - 1].split('?')[0].split('#')[0] if (noteId) { if (isSmallScreen || panelMode === 'single') { - // Single-pane / mobile: align stack with history (returning `pre` left stale UI). - openDrawer(noteId) + if (!isSmallScreen) { + openDrawer(noteId) + } const built = findAndCreateComponent(state.url, state.index) if (built.component) { return [ @@ -1834,7 +1838,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { } else if (newStack.length > 0) { // Stack still has items - update drawer to show the top item's note (for mobile/single-pane) // Only update drawer if drawer is currently open (not in the process of closing) - if ((isSmallScreen || panelMode === 'single') && drawerOpen && drawerNoteId) { + if (panelMode === 'single' && !isSmallScreen && drawerOpen && drawerNoteId) { // Extract noteId from top item's URL or from state.url const topItemUrl = newStack[newStack.length - 1]?.url || state?.url if (topItemUrl) { @@ -2153,7 +2157,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { } const syncDrawerToSecondaryStackTop = (stack: TStackItem[]) => { - if (!(isSmallScreen || panelMode === 'single')) return + if (isSmallScreen || panelMode !== 'single') return const top = stack[stack.length - 1] if (!top) return const noteId = noteHexIdFromSecondaryNoteUrl(top.url) @@ -2234,10 +2238,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { popSecondaryPageRef.current = popSecondaryPage const mobileSecondaryPanelOpen = - isSmallScreen && - secondaryStack.length > 0 && - !primaryNoteView && - !(drawerOpen && drawerNoteId) + isSmallScreen && secondaryStack.length > 0 && !primaryNoteView useMobileSwipeBackOnElement(mobileSecondaryPanelOpen ? mobileSecondarySwipeRoot : null, () => popSecondaryPageRef.current() , { @@ -2352,7 +2353,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { ) : ( <> - {secondaryStack.length > 0 && !(drawerOpen && drawerNoteId) ? ( + {secondaryStack.length > 0 ? (
)}
- {drawerNoteId && ( - { - if (open) { - setDrawerOpen(true) - return - } - hardCloseSecondaryPanel() - }} - noteId={drawerNoteId} - /> - )} diff --git a/src/components/ConnectedRelays/ActiveRelaysDropdownSection.tsx b/src/components/ConnectedRelays/ActiveRelaysDropdownSection.tsx new file mode 100644 index 00000000..55836eb0 --- /dev/null +++ b/src/components/ConnectedRelays/ActiveRelaysDropdownSection.tsx @@ -0,0 +1,58 @@ +import { useSmartRelayNavigation } from '@/PageManager' +import { + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator +} from '@/components/ui/dropdown-menu' +import { useRelayConnectionRows } from '@/hooks/useRelayConnectionRows' +import { toRelay } from '@/lib/link' +import { simplifyUrl } from '@/lib/url' +import { cn } from '@/lib/utils' +import { useTranslation } from 'react-i18next' +import RelayIcon from '../RelayIcon' + +function rowMuted(connected: boolean) { + return !connected +} + +function rowTitle(url: string, connected: boolean, t: (k: string) => string) { + const base = simplifyUrl(url) + if (!connected) return `${base} — ${t('Not connected')}` + return base +} + +function rowClass(connected: boolean) { + return cn(rowMuted(connected) && 'opacity-45 text-muted-foreground') +} + +/** Relay list block for account (or similar) dropdown menus. */ +export function ActiveRelaysDropdownSection() { + const { t } = useTranslation() + const { navigateToRelay } = useSmartRelayNavigation() + const { rows, connectedCount } = useRelayConnectionRows() + + if (rows.length === 0) return null + + const countSummary = `${connectedCount}/${rows.length}` + + return ( + <> + + + {t('Active relays')} + {countSummary} + + {rows.map(({ url, connected }) => ( + navigateToRelay(toRelay(url))} + className={cn('min-w-52 gap-2', rowClass(connected))} + > + + {simplifyUrl(url)} + + ))} + + ) +} diff --git a/src/components/ConnectedRelays/ActiveRelaysTitlebarButton.tsx b/src/components/ConnectedRelays/ActiveRelaysTitlebarButton.tsx deleted file mode 100644 index f340a488..00000000 --- a/src/components/ConnectedRelays/ActiveRelaysTitlebarButton.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useSmartRelayNavigation } from '@/PageManager' -import { Button } from '@/components/ui/button' -import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerOverlay } from '@/components/ui/drawer' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger -} from '@/components/ui/dropdown-menu' -import { useRelayConnectionRows } from '@/hooks/useRelayConnectionRows' -import { toRelay } from '@/lib/link' -import { simplifyUrl } from '@/lib/url' -import { cn } from '@/lib/utils' -import { useScreenSize } from '@/providers/ScreenSizeProvider' -import { Server } from 'lucide-react' -import { useState } from 'react' -import { useTranslation } from 'react-i18next' -import RelayIcon from '../RelayIcon' - -function rowMuted(connected: boolean) { - return !connected -} - -function rowTitle(url: string, connected: boolean, t: (k: string) => string) { - const base = simplifyUrl(url) - if (!connected) return `${base} — ${t('Not connected')}` - return base -} - -/** - * Server icon + menu listing relays with an open WebSocket in the pool. - */ -export function ActiveRelaysTitlebarButton() { - const { t } = useTranslation() - const { isSmallScreen } = useScreenSize() - const { navigateToRelay } = useSmartRelayNavigation() - const { rows, connectedCount } = useRelayConnectionRows() - const [drawerOpen, setDrawerOpen] = useState(false) - - const countSummary = - rows.length > 0 ? `${connectedCount}/${rows.length}` : '' - - const trigger = ( - - ) - - const rowClass = (connected: boolean) => - cn(rowMuted(connected) && 'opacity-45 text-muted-foreground') - - if (isSmallScreen) { - return ( - <> - {trigger} - - setDrawerOpen(false)} /> - - - {t('Active relays')} - {rows.length > 0 ? ( -

- {connectedCount} - / - {rows.length} -

- ) : null} -
-
- {rows.map(({ url, connected }) => ( - - ))} -
-
-
- - ) - } - - return ( - - {trigger} - - {t('Active relays')} - - {rows.map(({ url, connected }) => ( - navigateToRelay(toRelay(url))} - className={cn('min-w-52 gap-2', rowClass(connected))} - > - - {simplifyUrl(url)} - - ))} - - - ) -} diff --git a/src/components/DrawerMenuItem/index.tsx b/src/components/DrawerMenuItem/index.tsx index 341fd3cb..abe54805 100644 --- a/src/components/DrawerMenuItem/index.tsx +++ b/src/components/DrawerMenuItem/index.tsx @@ -2,6 +2,16 @@ import { Button } from '@/components/ui/button' import { DrawerClose } from '@/components/ui/drawer' import { cn } from '@/lib/utils' +/** Large-font / accessibility: wrap labels, top-align icons, scrollable sheet padding. */ +export const drawerMenuButtonClassName = + 'flex h-auto min-h-0 w-full items-start justify-start gap-3 whitespace-normal px-4 py-3 text-left text-base leading-snug [&_svg]:size-5 [&_svg]:shrink-0' + +export const drawerMenuContentClassName = + 'flex max-h-[min(90dvh,calc(100dvh-1rem))] flex-col overflow-hidden' + +export const drawerMenuScrollClassName = + 'min-h-0 flex-1 overflow-y-auto overscroll-contain px-1 pt-2 pb-[calc(1.25rem+env(safe-area-inset-bottom,0px))]' + export default function DrawerMenuItem({ children, className, @@ -15,7 +25,7 @@ export default function DrawerMenuItem({ - + ) } +function LoggedOutTitlebarMenu({ onLogin }: { onLogin: () => void }) { + const { t } = useTranslation() + const { rows } = useRelayConnectionRows() + + if (rows.length === 0) { + return ( + + ) + } + + return ( + + + + + + + + {t('Login')} + + + + + ) +} + /** Sidebar: account / login stack. Titlebar (mobile): compact account or login control. */ export default function HelpAndAccountMenu({ variant }: { variant: HelpAndAccountMenuVariant }) { - const { t } = useTranslation() const { pubkey, checkLogin } = useNostr() const { navigateToSettings } = useSmartSettingsNavigation() const onBrowseCache = useCallback(() => { @@ -218,18 +256,14 @@ export default function HelpAndAccountMenu({ variant }: { variant: HelpAndAccoun onBrowseCache={onBrowseCache} /> ) - } else if (variant === 'sidebar') { + } else if (variant === 'titlebar') { + account = checkLogin()} /> + } else { account = ( checkLogin()} title="Login"> ) - } else { - account = ( - - ) } const wrapClass = diff --git a/src/components/NoteOptions/DesktopMenu.tsx b/src/components/NoteOptions/DesktopMenu.tsx index 02e7dcd9..f7b338f5 100644 --- a/src/components/NoteOptions/DesktopMenu.tsx +++ b/src/components/NoteOptions/DesktopMenu.tsx @@ -18,6 +18,8 @@ interface DesktopMenuProps { menuActions: MenuAction[] trigger: React.ReactNode header?: React.ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void } function filterSubMenuRows( @@ -137,10 +139,16 @@ const MenuContent = memo( ) MenuContent.displayName = 'MenuContent' -export function DesktopMenu({ menuActions, trigger, header }: DesktopMenuProps) { +export function DesktopMenu({ menuActions, trigger, header, open, onOpenChange }: DesktopMenuProps) { const [subMenuFilter, setSubMenuFilter] = useState('') return ( - !open && setSubMenuFilter('')}> + { + onOpenChange?.(nextOpen) + if (!nextOpen) setSubMenuFilter('') + }} + > {trigger} {header} diff --git a/src/components/NoteOptions/MobileMenu.tsx b/src/components/NoteOptions/MobileMenu.tsx index aed793d7..7a6f7472 100644 --- a/src/components/NoteOptions/MobileMenu.tsx +++ b/src/components/NoteOptions/MobileMenu.tsx @@ -1,5 +1,10 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import { + drawerMenuButtonClassName, + drawerMenuContentClassName, + drawerMenuScrollClassName +} from '@/components/DrawerMenuItem' import { cn } from '@/lib/utils' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerOverlay } from '@/components/ui/drawer' import { ArrowLeft } from 'lucide-react' @@ -31,6 +36,29 @@ function filterSubMenuRows( return items.filter((s) => !s.filterHaystack || s.filterHaystack.includes(q)) } +function MobileMenuActionButton({ + icon: Icon, + label, + className, + onClick +}: { + icon: MenuAction['icon'] + label: React.ReactNode + className?: string + onClick?: () => void +}) { + return ( + + ) +} + export function MobileMenu({ menuActions, trigger, @@ -59,39 +87,38 @@ export function MobileMenu({ {trigger} - + Options -
+
{!showSubMenu ? ( <> {header} {menuActions.map((action, index) => { const Icon = action.icon return ( - + /> ) })} ) : ( <> - + />
{subMenuSearchable ? (
@@ -114,13 +141,10 @@ export function MobileMenu({ )) )} diff --git a/src/components/NoteOptions/index.tsx b/src/components/NoteOptions/index.tsx index b969d835..02b75106 100644 --- a/src/components/NoteOptions/index.tsx +++ b/src/components/NoteOptions/index.tsx @@ -63,6 +63,7 @@ export default function NoteOptions({ const [activeSubMenu, setActiveSubMenu] = useState([]) const [subMenuTitle, setSubMenuTitle] = useState('') const [subMenuSearchable, setSubMenuSearchable] = useState(false) + const [menuOpen, setMenuOpen] = useState(false) const closeDrawer = () => { setIsDrawerOpen(false) @@ -70,6 +71,12 @@ export default function NoteOptions({ setSubMenuSearchable(false) } + const closeDesktopMenu = () => { + setMenuOpen(false) + setShowSubMenu(false) + setSubMenuSearchable(false) + } + const goBackToMainMenu = () => { setShowSubMenu(false) setSubMenuSearchable(false) @@ -128,17 +135,15 @@ export default function NoteOptions({ [] ) - const menuHeader = useMemo( - () => ( + const menuHeader = + isDrawerOpen || menuOpen ? ( - ), - [event, seenOnAllowlist, isSmallScreen] - ) + ) : null return (
e.stopPropagation()}> @@ -157,7 +162,13 @@ export default function NoteOptions({ goBackToMainMenu={goBackToMainMenu} /> ) : ( - + )} setIsDrawerOpen(false)} /> - + {t('Boost')} -
+
diff --git a/src/components/NoteStats/SeenOnButton.tsx b/src/components/NoteStats/SeenOnButton.tsx index ae761e82..d82ea9b2 100644 --- a/src/components/NoteStats/SeenOnButton.tsx +++ b/src/components/NoteStats/SeenOnButton.tsx @@ -1,5 +1,10 @@ import { useSecondaryPage } from '@/PageManager' import { Button } from '@/components/ui/button' +import { + drawerMenuButtonClassName, + drawerMenuContentClassName, + drawerMenuScrollClassName +} from '@/components/DrawerMenuItem' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerOverlay } from '@/components/ui/drawer' import { DropdownMenu, @@ -55,14 +60,14 @@ export default function SeenOnButton({ {trigger} setIsDrawerOpen(false)} /> - + Seen on -
+
{relays.map((relay) => ( ))}
diff --git a/src/components/NoteStats/index.tsx b/src/components/NoteStats/index.tsx index 71262eeb..56845f6b 100644 --- a/src/components/NoteStats/index.tsx +++ b/src/components/NoteStats/index.tsx @@ -66,11 +66,6 @@ export default function NoteStats({ ? seenOnAllowlist : hintRelays /** At most two background refetches per card: before vs after inbox/favorite hints hydrate. */ - const statsRelayFetchTier = isRssArticleRoot ? relayMergeTier : hintRelays.length > 0 ? 1 : 0 - const statsRelaysRef = useRef(statsRelays) - statsRelaysRef.current = statsRelays - const seenOnAllowlistRef = useRef(seenOnAllowlist) - seenOnAllowlistRef.current = seenOnAllowlist const seenOnAllowlistKey = seenOnAllowlist?.length ? [...seenOnAllowlist] .map((u) => normalizeAnyRelayUrl(u) || u.trim()) @@ -78,6 +73,22 @@ export default function NoteStats({ .sort() .join('|') : '' + /** Home favorites feed: stats are scoped to the feed allowlist — ignore hint/current-relay churn. */ + const usesFeedStatsAllowlist = Boolean(seenOnAllowlistKey) + const statsRelayFetchTier = isRssArticleRoot + ? relayMergeTier + : usesFeedStatsAllowlist + ? 0 + : hintRelays.length > 0 + ? 1 + : 0 + const statsFetchRelayScopeKey = usesFeedStatsAllowlist + ? seenOnAllowlistKey + : `${statsRelayFetchTier}|${currentRelaysKey}` + const statsRelaysRef = useRef(statsRelays) + statsRelaysRef.current = statsRelays + const seenOnAllowlistRef = useRef(seenOnAllowlist) + seenOnAllowlistRef.current = seenOnAllowlist const shouldDeferStatsFetch = deferFetchUntilNearViewport ?? (fetchIfNotExisting && !foregroundStats) const containerRef = useRef(null) @@ -95,8 +106,7 @@ export default function NoteStats({ .finally(() => setLoading(false)) // Intentionally omit `event` object: parent feeds often pass new references each render; // id/sig/kind/created_at identify the note for refetch boundaries. - // `statsRelayFetchTier` (not full sorted relay key) avoids a REQ storm when favorites/current relays hydrate. - // `seenOnAllowlistKey` (not the array ref) avoids refetch loops when parents pass a new [] each render. + // `statsFetchRelayScopeKey` bundles tier + current relays, or feed allowlist only on home favorites. }, [ event.id, event.kind, @@ -107,9 +117,7 @@ export default function NoteStats({ shouldDeferStatsFetch, isNearViewport, pubkey, - statsRelayFetchTier, - currentRelaysKey, - seenOnAllowlistKey + statsFetchRelayScopeKey ]) const interactionButtons = ( diff --git a/src/hooks/useSeenOnRelays.ts b/src/hooks/useSeenOnRelays.ts index a11691e4..e0c5cb54 100644 --- a/src/hooks/useSeenOnRelays.ts +++ b/src/hooks/useSeenOnRelays.ts @@ -3,6 +3,10 @@ import { normalizeAnyRelayUrl } from '@/lib/url' import client from '@/services/client.service' import { useEffect, useRef, useState } from 'react' +function relayListsEqual(a: readonly string[], b: readonly string[]): boolean { + return a.length === b.length && a.every((url, i) => url === b[i]) +} + export function useSeenOnRelays( eventId: string, allowedRelays?: readonly string[] @@ -27,7 +31,9 @@ export function useSeenOnRelays( const allowlist = allowedRelaysRef.current const visible = allowlist?.length ? filterRelaysToUserAllowlist(seenOn, allowlist) : seenOn - if (!cancelled) setRelays(visible) + if (!cancelled) { + setRelays((prev) => (relayListsEqual(prev, visible) ? prev : visible)) + } return visible.length > 0 } if (apply()) return diff --git a/src/layouts/PrimaryPageLayout/index.tsx b/src/layouts/PrimaryPageLayout/index.tsx index aceee56d..df4dec1d 100644 --- a/src/layouts/PrimaryPageLayout/index.tsx +++ b/src/layouts/PrimaryPageLayout/index.tsx @@ -1,4 +1,4 @@ -import { ActiveRelaysTitlebarButton } from '@/components/ConnectedRelays/ActiveRelaysTitlebarButton' +import HelpAndAccountMenu from '@/components/HelpAndAccountMenu' import ScrollToTopButton from '@/components/ScrollToTopButton' import { ReadOnlySessionIndicator } from '@/components/ReadOnlySessionIndicator' import { Titlebar } from '@/components/Titlebar' @@ -26,8 +26,7 @@ const PrimaryPageLayout = forwardRef( pageName, displayScrollToTopButton = false, hideTitlebarBottomBorder = false, - subHeader, - suppressMobileDefaultActiveRelaysButton = false + subHeader }: { children?: React.ReactNode titlebar: React.ReactNode @@ -36,11 +35,6 @@ const PrimaryPageLayout = forwardRef( hideTitlebarBottomBorder?: boolean /** Rendered between titlebar and scroll area; not in scroll flow so it never overlaps content */ subHeader?: React.ReactNode - /** - * When true on small screens, omit the trailing {@link ActiveRelaysTitlebarButton} so the page can - * place it next to the account control (e.g. feed titlebar). - */ - suppressMobileDefaultActiveRelaysButton?: boolean }, ref ) => { @@ -140,10 +134,7 @@ const PrimaryPageLayout = forwardRef( }} > {hasTitlebarRow ? ( - + {titlebar} ) : null} @@ -164,10 +155,7 @@ const PrimaryPageLayout = forwardRef( >
{hasTitlebarRow ? ( - + {titlebar} ) : null} @@ -197,16 +185,12 @@ export type TPrimaryPageLayoutRef = { function PrimaryPageTitlebar({ children, - hideBottomBorder = false, - suppressMobileActiveRelays = false + hideBottomBorder = false }: { children?: React.ReactNode hideBottomBorder?: boolean - suppressMobileActiveRelays?: boolean }) { const { isSmallScreen } = useScreenSize() - /** Desktop: relay strip lives in the sidebar only. Narrow screens: titlebar control (or inline on feed). */ - const showTrailingActiveRelays = isSmallScreen && !suppressMobileActiveRelays return (
{children}
- {showTrailingActiveRelays ? : null} + {isSmallScreen ? : null}
) diff --git a/src/layouts/SecondaryPageLayout/index.tsx b/src/layouts/SecondaryPageLayout/index.tsx index f1cd4ea1..b9157a8c 100644 --- a/src/layouts/SecondaryPageLayout/index.tsx +++ b/src/layouts/SecondaryPageLayout/index.tsx @@ -1,4 +1,4 @@ -import { ActiveRelaysTitlebarButton } from '@/components/ConnectedRelays/ActiveRelaysTitlebarButton' +import HelpAndAccountMenu from '@/components/HelpAndAccountMenu' import ScrollToTopButton from '@/components/ScrollToTopButton' import { ReadOnlySessionIndicator } from '@/components/ReadOnlySessionIndicator' import { Titlebar } from '@/components/Titlebar' @@ -175,7 +175,7 @@ function SecondaryPageTitlebar({
{titlebar}
- {isSmallScreen ? : null} + {isSmallScreen ? : null}
) @@ -203,7 +203,7 @@ function SecondaryPageTitlebar({ {controls}
- {isSmallScreen ? : null} + {isSmallScreen ? : null}
) diff --git a/src/lib/home-feed-relays.ts b/src/lib/home-feed-relays.ts index 123530cd..69d8a348 100644 --- a/src/lib/home-feed-relays.ts +++ b/src/lib/home-feed-relays.ts @@ -2,14 +2,11 @@ import { MAX_REQ_RELAY_URLS } from '@/constants' import { feedRelayPolicyUrls } from '@/features/feed/relay-policy' import { getHttpRelayListFromEvent, getRelayListReadFromEventNoFastFallback } from '@/lib/event-metadata' import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays' -import { relayUrlIsAggrNostrLand } from '@/lib/nostr-land-relay-eligibility' +import { stripNostrLandAggrFromRelayUrls } from '@/lib/nostr-land-relay-eligibility' import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults' import type { Event } from 'nostr-tools' -/** Drop nostr.land aggregate from REQ stacks where it must not appear (e.g. home feeds). */ -export function stripNostrLandAggrFromRelayUrls(urls: readonly string[]): string[] { - return urls.filter((url) => !relayUrlIsAggrNostrLand(url)) -} +export { stripNostrLandAggrFromRelayUrls } /** * Home timeline REQs (Notes, Replies, and Gallery tabs on `home-all-favorites`) must never hit aggr — only diff --git a/src/lib/nostr-land-relay-eligibility.ts b/src/lib/nostr-land-relay-eligibility.ts index e39c23fd..e21bde20 100644 --- a/src/lib/nostr-land-relay-eligibility.ts +++ b/src/lib/nostr-land-relay-eligibility.ts @@ -19,6 +19,11 @@ export function relayUrlIsAggrNostrLand(url: string): boolean { } } +/** Drop `wss://aggr.nostr.land` from REQ stacks where it must not appear (e.g. home feeds). */ +export function stripNostrLandAggrFromRelayUrls(urls: readonly string[]): string[] { + return urls.filter((url) => !relayUrlIsAggrNostrLand(url)) +} + /** * True when any URL is the canonical nostr.land **inbox** relay (`wss://nostr.land`), i.e. host `nostr.land` * exactly — not `aggr.nostr.land`, `hist.nostr.land`, or other subdomains. diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx index 7c800487..976792ee 100644 --- a/src/pages/primary/NoteListPage/index.tsx +++ b/src/pages/primary/NoteListPage/index.tsx @@ -18,8 +18,6 @@ import React, { } from 'react' import { useTranslation } from 'react-i18next' import { FavoriteRelaysActiveStripMobileBar } from '@/components/FavoriteRelaysActiveStrip' -import { ActiveRelaysTitlebarButton } from '@/components/ConnectedRelays/ActiveRelaysTitlebarButton' -import HelpAndAccountMenu from '@/components/HelpAndAccountMenu' import Logo from '@/assets/Logo' import RelaysFeed from './RelaysFeed' import { usePrimaryPage } from '@/contexts/primary-page-context' @@ -81,7 +79,6 @@ const NoteListPage = forwardRef((_, ref) => { @@ -205,8 +202,6 @@ function NoteListPageTitlebar({
{showTitlebarRefresh ? : null} - -
) diff --git a/src/services/note-stats.service.ts b/src/services/note-stats.service.ts index cef2bf1f..ce40c118 100644 --- a/src/services/note-stats.service.ts +++ b/src/services/note-stats.service.ts @@ -23,7 +23,10 @@ import { import { eventReferencesThreadTarget, threadRootRefFromStatsRootEvent } from '@/lib/op-reference-tags' import type { TThreadRootRef } from '@/lib/thread-reply-root-match' import { filterRelaysToUserAllowlist, isRelayInUserAllowlist } from '@/lib/relay-allowlist' -import { prependAggrNostrLandIfViewerEligible } from '@/lib/nostr-land-relay-eligibility' +import { + prependAggrNostrLandIfViewerEligible, + stripNostrLandAggrFromRelayUrls +} from '@/lib/nostr-land-relay-eligibility' import { buildComprehensiveRelayList, relayHintsFromEventTags } from '@/lib/relay-list-builder' import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority' import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults' @@ -230,6 +233,11 @@ class NoteStatsService { return } + /** Background feed cards: one relay wave per note — effect re-runs must not stack REQs. */ + if (!foreground && this.noteStatsMap.get(eventId)?.updatedAt != null) { + return + } + if (this.processingCache.has(eventId)) { this.mergeFavoriteRelaysIntoDeferred(eventId, favoriteRelays) rememberRoot() @@ -632,10 +640,15 @@ class NoteStatsService { if (relayAllowlist?.length) { const onAllowlist = (u: string) => isRelayInUserAllowlist(u, relayAllowlist) - return this.finalizeNoteStatsRelayUrls( - filterRelaysToUserAllowlist( - [...relayAllowlist, ...relayHints.filter(onAllowlist)], - relayAllowlist + // Match home feed timeline policy: allowlisted stats must not hit aggr.nostr.land. + return stripNostrLandAggrFromRelayUrls( + sanitizeRelayUrlsForFetch( + dedupeNormalizeRelayUrlsOrdered( + filterRelaysToUserAllowlist( + [...relayAllowlist, ...relayHints.filter(onAllowlist)], + relayAllowlist + ) + ) ) ) }