You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
121 lines
4.1 KiB
121 lines
4.1 KiB
import { Skeleton } from '@/components/ui/skeleton' |
|
import { SEARCHABLE_RELAY_URLS } from '@/constants' |
|
import { useFetchEvent } from '@/hooks' |
|
import { cn } from '@/lib/utils' |
|
import client from '@/services/client.service' |
|
import { useTranslation } from 'react-i18next' |
|
import { useCallback, useEffect, useRef, useState } from 'react' |
|
import { Event } from 'nostr-tools' |
|
import ContentPreview from '../ContentPreview' |
|
import UserAvatar from '../UserAvatar' |
|
import logger from '@/lib/logger' |
|
|
|
export default function ParentNotePreview({ |
|
eventId, |
|
className, |
|
onClick, |
|
/** Inline hint without pill background (e.g. reply thread rows). */ |
|
appearance = 'default' |
|
}: { |
|
eventId: string |
|
className?: string |
|
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined |
|
appearance?: 'default' | 'subtle' |
|
}) { |
|
const { t } = useTranslation() |
|
const { event, isFetching } = useFetchEvent(eventId) |
|
const [fallbackEvent, setFallbackEvent] = useState<Event | undefined>(undefined) |
|
const [isFetchingFallback, setIsFetchingFallback] = useState(false) |
|
/** One automatic searchable-relay attempt per eventId; without this, the effect re-fires forever after each 20s timeout. */ |
|
const autoSearchableAttemptedRef = useRef(false) |
|
|
|
// Helper function to fetch from searchable relays (hex, note1, nevent1, naddr1) |
|
const fetchFromSearchableRelays = useCallback(async () => { |
|
if (!eventId?.trim()) return |
|
|
|
setIsFetchingFallback(true) |
|
try { |
|
const foundEvent = await client.fetchEventWithExternalRelays(eventId, SEARCHABLE_RELAY_URLS) |
|
if (foundEvent) { |
|
client.addEventToCache(foundEvent) |
|
setFallbackEvent(foundEvent) |
|
} |
|
} catch (error) { |
|
logger.warn('Fallback fetch from searchable relays failed', error as Error) |
|
} finally { |
|
setIsFetchingFallback(false) |
|
} |
|
}, [eventId]) |
|
|
|
useEffect(() => { |
|
autoSearchableAttemptedRef.current = false |
|
}, [eventId]) |
|
|
|
// If the initial fetch fails, try searchable relays once (manual retry still works via onClick). |
|
useEffect(() => { |
|
if ( |
|
!isFetching && |
|
!event && |
|
!fallbackEvent && |
|
!isFetchingFallback && |
|
eventId && |
|
!autoSearchableAttemptedRef.current |
|
) { |
|
autoSearchableAttemptedRef.current = true |
|
void fetchFromSearchableRelays() |
|
} |
|
}, [isFetching, event, eventId, fallbackEvent, isFetchingFallback, fetchFromSearchableRelays]) |
|
|
|
const finalEvent = event || fallbackEvent |
|
const finalIsFetching = isFetching || isFetchingFallback |
|
|
|
const shellClass = |
|
appearance === 'subtle' |
|
? 'flex gap-1.5 items-center text-xs w-full max-w-full text-muted-foreground' |
|
: 'flex gap-1 items-center text-sm rounded-full px-2 bg-muted w-fit max-w-full text-muted-foreground' |
|
|
|
if (finalIsFetching) { |
|
return ( |
|
<div data-parent-note-preview className={cn(shellClass, appearance === 'subtle' && 'w-full', className)}> |
|
<div className="shrink-0">{t('reply to')}</div> |
|
<Skeleton className="w-4 h-4 rounded-full" /> |
|
<div className={cn('flex-1 min-w-0', appearance === 'subtle' ? 'py-0' : 'py-1')}> |
|
<Skeleton className="h-3" /> |
|
</div> |
|
</div> |
|
) |
|
} |
|
|
|
// Handle click for retry when event not found |
|
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => { |
|
if (finalEvent) { |
|
onClick?.(e) |
|
} else if (!finalEvent && !finalIsFetching && eventId) { |
|
// Retry fetch from searchable relays when clicking "Note not found" |
|
e.stopPropagation() |
|
fetchFromSearchableRelays() |
|
} |
|
} |
|
|
|
return ( |
|
<div |
|
data-parent-note-preview |
|
className={cn( |
|
shellClass, |
|
(finalEvent || (!finalEvent && !finalIsFetching)) && 'hover:text-foreground cursor-pointer', |
|
className |
|
)} |
|
onClick={handleClick} |
|
> |
|
<div className="shrink-0">{t('reply to')}</div> |
|
{finalEvent && <UserAvatar className="shrink-0" userId={finalEvent.pubkey} size="tiny" />} |
|
<div className="truncate flex-1 min-w-0"> |
|
<ContentPreview |
|
className="pointer-events-none" |
|
event={finalEvent} |
|
previewDensity={appearance === 'subtle' ? 'compact' : 'default'} |
|
/> |
|
</div> |
|
</div> |
|
) |
|
}
|
|
|