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

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>
)
}