7 changed files with 240 additions and 48 deletions
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
import { getNoteBech32Id } from '@/lib/event' |
||||
import client from '@/services/client.service' |
||||
import { navigationEventStore } from '@/services/navigation-event-store' |
||||
import type { Event } from 'nostr-tools' |
||||
|
||||
function initialEventMatchesId(initialEvent: Event, eventId: string): boolean { |
||||
if (initialEvent.id === eventId) return true |
||||
try { |
||||
return getNoteBech32Id(initialEvent) === eventId |
||||
} catch { |
||||
return false |
||||
} |
||||
} |
||||
|
||||
/** Synchronous note lookup: navigation store → session cache (feed clicks seed both before the panel mounts). */ |
||||
export function resolveNoteEventSync(eventId: string | undefined, initialEvent?: Event): Event | undefined { |
||||
if (!eventId?.trim()) return initialEvent |
||||
|
||||
if (initialEvent && initialEventMatchesId(initialEvent, eventId)) { |
||||
return initialEvent |
||||
} |
||||
|
||||
const fromNav = navigationEventStore.peekEvent(eventId) |
||||
if (fromNav) return fromNav |
||||
|
||||
return client.peekSessionCachedEvent(eventId.trim()) |
||||
} |
||||
@ -0,0 +1,128 @@
@@ -0,0 +1,128 @@
|
||||
import ContentPreview from '@/components/ContentPreview' |
||||
import UserAvatar from '@/components/UserAvatar' |
||||
import { Skeleton } from '@/components/ui/skeleton' |
||||
import { Separator } from '@/components/ui/separator' |
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' |
||||
import { |
||||
getParentBech32Id, |
||||
getParentEventHexId, |
||||
getRootBech32Id, |
||||
getRootEventHexId |
||||
} from '@/lib/event' |
||||
import { getCachedThreadContextEvents } from '@/lib/navigation-related-events' |
||||
import { resolveNoteEventSync } from '@/lib/resolve-note-event-sync' |
||||
import client from '@/services/client.service' |
||||
import type { Event } from 'nostr-tools' |
||||
import { useTranslation } from 'react-i18next' |
||||
|
||||
function peekThreadContextEvent(bech32OrHex: string | undefined): Event | undefined { |
||||
if (!bech32OrHex?.trim()) return undefined |
||||
return client.peekSessionCachedEvent(bech32OrHex.trim()) |
||||
} |
||||
|
||||
function ThreadContextPreview({ event }: { event: Event }) { |
||||
return ( |
||||
<div className="mb-2 rounded-lg border border-border/80 bg-muted/20 px-3 py-2 opacity-90"> |
||||
<div className="flex items-center gap-2"> |
||||
<UserAvatar userId={event.pubkey} size="small" className="shrink-0" deferRemoteAvatar={false} /> |
||||
<div className="min-w-0 flex-1 truncate text-sm text-muted-foreground"> |
||||
<ContentPreview event={event} /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
/** |
||||
* Shown while the lazy NotePage chunk loads. Renders the clicked note from the navigation/session |
||||
* cache when available so the secondary panel is not blank. |
||||
*/ |
||||
export default function NotePageInstantShell({ |
||||
id, |
||||
index, |
||||
hideTitlebar = false, |
||||
initialEvent |
||||
}: { |
||||
id?: string |
||||
index?: number |
||||
hideTitlebar?: boolean |
||||
initialEvent?: Event |
||||
}) { |
||||
const { t } = useTranslation() |
||||
const event = resolveNoteEventSync(id, initialEvent) |
||||
|
||||
if (!event) { |
||||
return ( |
||||
<SecondaryPageLayout |
||||
index={index} |
||||
title={hideTitlebar ? undefined : t('Note')} |
||||
> |
||||
<div className="px-4 pt-3"> |
||||
<div className="flex items-center space-x-2"> |
||||
<Skeleton className="h-10 w-10 rounded-full" /> |
||||
<div className="flex-1 w-0"> |
||||
<div className="py-1"> |
||||
<Skeleton className="h-4 w-16" /> |
||||
</div> |
||||
<div className="py-0.5"> |
||||
<Skeleton className="h-3 w-12" /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div className="pt-2"> |
||||
<div className="my-1"> |
||||
<Skeleton className="my-1 mt-2 h-4 w-full" /> |
||||
</div> |
||||
<div className="my-1"> |
||||
<Skeleton className="my-1 h-4 w-2/3" /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</SecondaryPageLayout> |
||||
) |
||||
} |
||||
|
||||
const cachedContext = getCachedThreadContextEvents(event) |
||||
const parentBech32 = getParentBech32Id(event) |
||||
const rootBech32 = getRootBech32Id(event) |
||||
const parentHex = getParentEventHexId(event)?.toLowerCase() |
||||
const rootHex = getRootEventHexId(event)?.toLowerCase() |
||||
const selfHex = event.id.toLowerCase() |
||||
|
||||
const parentEvent = |
||||
cachedContext.find((e) => parentHex && e.id.toLowerCase() === parentHex) ?? |
||||
peekThreadContextEvent(parentBech32) |
||||
const rootEvent = |
||||
cachedContext.find((e) => rootHex && e.id.toLowerCase() === rootHex) ?? |
||||
peekThreadContextEvent(rootBech32) |
||||
|
||||
const parentEventForStrip = |
||||
parentEvent && parentEvent.id.toLowerCase() !== selfHex ? parentEvent : undefined |
||||
const rootEventForStrip = |
||||
rootEvent && rootEvent.id.toLowerCase() !== selfHex ? rootEvent : undefined |
||||
|
||||
return ( |
||||
<SecondaryPageLayout index={index} title={hideTitlebar ? undefined : t('Note')}> |
||||
<div className="w-full px-4 pt-3"> |
||||
{rootEventForStrip && parentEventForStrip?.id !== rootEventForStrip.id && ( |
||||
<ThreadContextPreview event={rootEventForStrip} /> |
||||
)} |
||||
{parentEventForStrip && ( |
||||
<> |
||||
<ThreadContextPreview event={parentEventForStrip} /> |
||||
<div className="ml-5 h-3 w-px bg-border" /> |
||||
</> |
||||
)} |
||||
{(rootEventForStrip || parentEventForStrip) && <Separator className="my-3" />} |
||||
<div className="select-text"> |
||||
<div className="flex items-start gap-2"> |
||||
<UserAvatar userId={event.pubkey} size="normal" className="shrink-0" deferRemoteAvatar={false} /> |
||||
<div className="min-w-0 flex-1"> |
||||
<ContentPreview event={event} className="text-base text-foreground whitespace-pre-wrap break-words" /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</SecondaryPageLayout> |
||||
) |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
import type { TPageRef } from '@/types' |
||||
import type { Event } from 'nostr-tools' |
||||
import { forwardRef, lazy, Suspense } from 'react' |
||||
import NotePageInstantShell from './NotePageInstantShell' |
||||
|
||||
const NotePageLazy = lazy(() => import('./index')) |
||||
|
||||
/** Sync entry: instant shell while the lazy NotePage chunk loads. */ |
||||
const NotePageRoute = forwardRef< |
||||
TPageRef, |
||||
{ id?: string; index?: number; hideTitlebar?: boolean; initialEvent?: Event } |
||||
>((props, ref) => ( |
||||
<Suspense fallback={<NotePageInstantShell {...props} />}> |
||||
<NotePageLazy {...props} ref={ref} /> |
||||
</Suspense> |
||||
)) |
||||
NotePageRoute.displayName = 'NotePageRoute' |
||||
export default NotePageRoute |
||||
|
||||
/** Warm the note panel chunk on feed hover / pointer-down. */ |
||||
export function preloadNotePageChunk(): void { |
||||
void import('./index') |
||||
} |
||||
Loading…
Reference in new issue