diff --git a/src/PageManager.tsx b/src/PageManager.tsx index e598134a..d697fb73 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -7,6 +7,7 @@ import { RefreshButton } from '@/components/RefreshButton' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' import logger from '@/lib/logger' +import { captureMobilePrimaryFeedScrollFromWindow, peekMobilePrimaryFeedScroll } from '@/lib/mobile-primary-feed-scroll' import { useMobileSwipeBackOnElement } from '@/lib/mobile-swipe-back' import { preventRadixSheetCloseForPortaledOverlay } from '@/lib/sheet-dismiss-guard' import { ChevronLeft } from 'lucide-react' @@ -2033,6 +2034,10 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { noteStatsService.setBackgroundStatsPaused(true) client.interruptBackgroundQueries() + if (isSmallScreen && currentPrimaryPage) { + captureMobilePrimaryFeedScrollFromWindow(currentPrimaryPage) + } + // Small screens render either the primary overlay OR the secondary stack — not both. // Clear overlays (e.g. full-screen note) so pushes from Seen-on, settings deep links, etc. show the target page. if (isSmallScreen && primaryNoteView) { @@ -2109,6 +2114,12 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { ) currentTabStateRef.current.set(page, savedFeedState.tab) } + if (isSmallScreen) { + const top = peekMobilePrimaryFeedScroll(page) + requestAnimationFrame(() => { + window.scrollTo({ top, behavior: 'instant' }) + }) + } } const hardCloseSecondaryPanel = () => { @@ -2223,7 +2234,10 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { popSecondaryPageRef.current = popSecondaryPage const mobileSecondaryPanelOpen = - isSmallScreen && secondaryStack.length > 0 && !primaryNoteView + isSmallScreen && + secondaryStack.length > 0 && + !primaryNoteView && + !(drawerOpen && drawerNoteId) useMobileSwipeBackOnElement(mobileSecondaryPanelOpen ? mobileSecondarySwipeRoot : null, () => popSecondaryPageRef.current() , { @@ -2338,7 +2352,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { ) : ( <> - {secondaryStack.length > 0 ? ( + {secondaryStack.length > 0 && !(drawerOpen && drawerNoteId) ? (
)} {nip05Domain} diff --git a/src/components/Nip05DomainPanel/Nip05DomainEmptyState.tsx b/src/components/Nip05DomainPanel/Nip05DomainEmptyState.tsx new file mode 100644 index 00000000..c6cd41a1 --- /dev/null +++ b/src/components/Nip05DomainPanel/Nip05DomainEmptyState.tsx @@ -0,0 +1,14 @@ +import { useTranslation } from 'react-i18next' +import WellKnownNip05UrlLink from './WellKnownNip05UrlLink' + +export default function Nip05DomainEmptyState({ domain }: { domain: string }) { + const { t } = useTranslation() + return ( +
+

{t('No pubkeys found on NIP-05 domain')}

+

+ +

+
+ ) +} diff --git a/src/components/Nip05DomainPanel/ProfileListByNip05Domain.tsx b/src/components/Nip05DomainPanel/ProfileListByNip05Domain.tsx new file mode 100644 index 00000000..6ffce273 --- /dev/null +++ b/src/components/Nip05DomainPanel/ProfileListByNip05Domain.tsx @@ -0,0 +1,83 @@ +import UserItem from '@/components/UserItem' +import { Skeleton } from '@/components/ui/skeleton' +import { fetchNip05NamePubkeysFromDomain } from '@/lib/nip05' +import { useEffect, useMemo, useRef, useState } from 'react' +import Nip05DomainEmptyState from './Nip05DomainEmptyState' + +export type TNip05NamePubkey = { name: string; pubkey: string } + +export default function ProfileListByNip05Domain({ domain }: { domain: string }) { + const [entries, setEntries] = useState(null) + const [visibleCount, setVisibleCount] = useState(10) + const bottomRef = useRef(null) + const entriesKey = useMemo( + () => (entries ? entries.map((e) => `${e.name}:${e.pubkey}`).join('\u0001') : ''), + [entries] + ) + + useEffect(() => { + let cancelled = false + setEntries(null) + setVisibleCount(10) + void fetchNip05NamePubkeysFromDomain(domain).then((rows) => { + if (!cancelled) setEntries(rows) + }) + return () => { + cancelled = true + } + }, [domain]) + + useEffect(() => { + if (!entries?.length) return + setVisibleCount(10) + }, [entriesKey, entries]) + + useEffect(() => { + if (!entries?.length) return + const options = { root: null, rootMargin: '10px', threshold: 1 } + const observer = new IntersectionObserver((obs) => { + if (obs[0]?.isIntersecting && visibleCount < entries.length) { + setVisibleCount((n) => Math.min(n + 10, entries.length)) + } + }, options) + const node = bottomRef.current + if (node) observer.observe(node) + return () => { + if (node) observer.unobserve(node) + } + }, [visibleCount, entriesKey, entries]) + + if (entries === null) { + return ( +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+ ) + } + + if (entries.length === 0) { + return + } + + const visible = entries.slice(0, visibleCount) + return ( +
+ {visible.map(({ name, pubkey }) => ( +
+ {name && name !== '_' ? ( + {name} + ) : null} +
+ +
+
+ ))} + {visibleCount < entries.length ?
: null} +
+ ) +} diff --git a/src/components/Nip05DomainPanel/WellKnownNip05UrlLink.tsx b/src/components/Nip05DomainPanel/WellKnownNip05UrlLink.tsx new file mode 100644 index 00000000..ef446007 --- /dev/null +++ b/src/components/Nip05DomainPanel/WellKnownNip05UrlLink.tsx @@ -0,0 +1,25 @@ +import { getWellKnownNip05Url } from '@/lib/nip05' +import { cn } from '@/lib/utils' + +export default function WellKnownNip05UrlLink({ + domain, + className +}: { + domain: string + className?: string +}) { + const url = getWellKnownNip05Url(domain) + return ( + + {url} + + ) +} diff --git a/src/components/Nip05List/index.tsx b/src/components/Nip05List/index.tsx index 2e856613..4555eea9 100644 --- a/src/components/Nip05List/index.tsx +++ b/src/components/Nip05List/index.tsx @@ -1,6 +1,6 @@ import { Skeleton } from '@/components/ui/skeleton' import { splitNip05Identifier, verifyNip05 } from '@/lib/nip05' -import { toNoteList } from '@/lib/link' +import { toProfileList } from '@/lib/link' import { SecondaryPageLink } from '@/PageManager' import { BadgeAlert, BadgeCheck } from 'lucide-react' import { Favicon } from '../Favicon' @@ -114,7 +114,7 @@ export default function Nip05List({ nip05List, pubkey }: { nip05List: string[]; )} {nip05Domain} diff --git a/src/components/NormalFeed/index.tsx b/src/components/NormalFeed/index.tsx index 649362d4..5aa93183 100644 --- a/src/components/NormalFeed/index.tsx +++ b/src/components/NormalFeed/index.tsx @@ -348,7 +348,7 @@ const NormalFeed = forwardRef{tabsElement}
) @@ -375,7 +375,7 @@ const NormalFeed = forwardRef{tabsElement}
diff --git a/src/components/NoteDrawer/index.tsx b/src/components/NoteDrawer/index.tsx index a5649a3d..fa8286ef 100644 --- a/src/components/NoteDrawer/index.tsx +++ b/src/components/NoteDrawer/index.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect, useRef } from 'react' -import { useMobileSwipeBackOnElement } from '@/lib/mobile-swipe-back' +import { useState, useEffect, useRef, useCallback } from 'react' +import { MOBILE_SWIPE_BACK_EDGE_PX, useMobileSwipeBackOnElement } from '@/lib/mobile-swipe-back' import { preventRadixSheetCloseForPortaledOverlay } from '@/lib/sheet-dismiss-guard' import { Sheet, SheetContent } from '@/components/ui/sheet' import NotePage from '@/pages/secondary/NotePage' @@ -16,23 +16,43 @@ interface NoteDrawerProps { export default function NoteDrawer({ open, onOpenChange, noteId, initialEvent }: NoteDrawerProps) { const { currentIndex, pop } = useSecondaryPage() const [displayNoteId, setDisplayNoteId] = useState(noteId) - const [swipeRoot, setSwipeRoot] = useState(null) + const [swipeEdge, setSwipeEdge] = useState(null) const timeoutRef = useRef(null) + const closingFromSwipeRef = useRef(false) - useMobileSwipeBackOnElement(open ? swipeRoot : null, pop, { enabled: open }) + const handleSwipeBack = useCallback(() => { + if (!open || closingFromSwipeRef.current) return + closingFromSwipeRef.current = true + pop() + }, [open, pop]) + + useEffect(() => { + if (open) closingFromSwipeRef.current = false + }, [open, noteId]) + + useMobileSwipeBackOnElement(open ? swipeEdge : null, handleSwipeBack, { + enabled: open, + edgePx: MOBILE_SWIPE_BACK_EDGE_PX + }) + + useEffect(() => { + if (!open) return + const prev = document.body.style.overflow + document.body.style.overflow = 'hidden' + return () => { + document.body.style.overflow = prev + } + }, [open]) useEffect(() => { - // Clear any pending timeout if (timeoutRef.current) { clearTimeout(timeoutRef.current) timeoutRef.current = null } if (noteId) { - // New noteId - show immediately setDisplayNoteId(noteId) } else if (!open && displayNoteId) { - // Closing - keep content visible during animation (350ms) timeoutRef.current = setTimeout(() => { setDisplayNoteId(null) timeoutRef.current = null @@ -49,19 +69,21 @@ export default function NoteDrawer({ open, onOpenChange, noteId, initialEvent }: if (!displayNoteId) return null return ( - + preventRadixSheetCloseForPortaledOverlay(e)} onInteractOutside={(e) => preventRadixSheetCloseForPortaledOverlay(e)} > -
+
+
-
- -
- {feedClientFilterOpen ? ( -
+ const feedClientFilterToggleButton = ( + + ) + + const feedClientFilterPanel = feedClientFilterOpen ? ( +