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.
125 lines
4.7 KiB
125 lines
4.7 KiB
import { Event, nip19 } from 'nostr-tools' |
|
import { useMemo, useState } from 'react' |
|
import { useTranslation } from 'react-i18next' |
|
import dayjs from 'dayjs' |
|
import { Button } from '@/components/ui/button' |
|
import { Copy, Check } from 'lucide-react' |
|
import { toast } from 'sonner' |
|
import logger from '@/lib/logger' |
|
import { cn } from '@/lib/utils' |
|
import { isRssThreadSyntheticParentEvent } from '@/lib/rss-article' |
|
import { isValidPubkey } from '@/lib/pubkey' |
|
import { getKindDescription } from '@/lib/kind-description' |
|
import UserAvatar from '@/components/UserAvatar' |
|
import Username from '@/components/Username' |
|
|
|
function isAllZeroPlaceholderPubkey(pk: string): boolean { |
|
return isValidPubkey(pk) && /^0+$/.test(pk) |
|
} |
|
|
|
export default function EventViewer({ |
|
event, |
|
className |
|
}: { |
|
event: Event |
|
className?: string |
|
}) { |
|
const { t } = useTranslation() |
|
const [copiedJson, setCopiedJson] = useState(false) |
|
const [copiedNevent, setCopiedNevent] = useState(false) |
|
|
|
const nevent = useMemo( |
|
() => nip19.neventEncode({ id: event.id, author: event.pubkey, kind: event.kind }), |
|
[event.id, event.pubkey, event.kind] |
|
) |
|
|
|
const jsonPretty = useMemo(() => JSON.stringify(event, null, 2), [event]) |
|
|
|
const handleCopyJson = async () => { |
|
try { |
|
await navigator.clipboard.writeText(jsonPretty) |
|
setCopiedJson(true) |
|
toast.success(t('Copied to clipboard')) |
|
setTimeout(() => setCopiedJson(false), 2000) |
|
} catch (err) { |
|
logger.error('Failed to copy event JSON', { error: err, eventId: event.id }) |
|
toast.error(t('Failed to copy')) |
|
} |
|
} |
|
|
|
const handleCopyNevent = async () => { |
|
try { |
|
await navigator.clipboard.writeText(nevent) |
|
setCopiedNevent(true) |
|
toast.success(t('Copied to clipboard')) |
|
setTimeout(() => setCopiedNevent(false), 2000) |
|
} catch (err) { |
|
logger.error('Failed to copy nevent', { error: err }) |
|
toast.error(t('Failed to copy')) |
|
} |
|
} |
|
|
|
const createdAtFormatted = dayjs(event.created_at * 1000).format('LLL') |
|
const pubkey = event.pubkey ?? '' |
|
const hidePubkeyRow = isRssThreadSyntheticParentEvent(event) |
|
const showAuthorBadge = |
|
!hidePubkeyRow && isValidPubkey(pubkey) && !isAllZeroPlaceholderPubkey(pubkey) |
|
const kindLabel = getKindDescription(event.kind) |
|
|
|
return ( |
|
<div className={cn('rounded-lg border border-border bg-muted/20 p-4', className)}> |
|
<div className="flex flex-wrap items-start justify-between gap-2 border-b border-border/60 pb-3"> |
|
<div> |
|
<div className="text-sm font-semibold text-foreground">{kindLabel.description}</div> |
|
<div className="mt-0.5 text-xs text-muted-foreground"> |
|
{t('Event kind and time', { kind: event.kind, time: createdAtFormatted })} |
|
</div> |
|
</div> |
|
<Button variant="outline" size="sm" onClick={handleCopyJson} className="h-8 shrink-0 gap-1.5"> |
|
{copiedJson ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />} |
|
{t('Copy JSON')} |
|
</Button> |
|
</div> |
|
|
|
<div className="mt-3 space-y-2 text-sm"> |
|
<div className="flex flex-wrap items-center gap-2"> |
|
<span className="shrink-0 font-medium text-muted-foreground">{t('nevent')}</span> |
|
<code className="min-w-0 flex-1 truncate rounded bg-muted/80 px-2 py-0.5 font-mono text-xs text-foreground"> |
|
{nevent} |
|
</code> |
|
<Button variant="ghost" size="sm" onClick={handleCopyNevent} className="h-7 w-7 shrink-0 p-0"> |
|
{copiedNevent ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />} |
|
</Button> |
|
</div> |
|
{!hidePubkeyRow && ( |
|
<div className="flex flex-wrap items-center gap-2"> |
|
<span className="shrink-0 font-medium text-muted-foreground">{t('Author')}</span> |
|
{showAuthorBadge ? ( |
|
<div className="flex min-w-0 items-center gap-1.5"> |
|
<UserAvatar userId={pubkey} size="xSmall" /> |
|
<Username |
|
userId={pubkey} |
|
className="min-w-0 font-normal" |
|
skeletonClassName="h-4" |
|
withoutSkeleton |
|
/> |
|
</div> |
|
) : ( |
|
<span className="break-all text-xs text-muted-foreground"> |
|
{!pubkey |
|
? t('Missing pubkey') |
|
: isAllZeroPlaceholderPubkey(pubkey) |
|
? t('Synthetic event (no author)') |
|
: pubkey} |
|
</span> |
|
)} |
|
</div> |
|
)} |
|
</div> |
|
|
|
<pre className="mt-4 max-h-[min(50vh,28rem)] overflow-auto rounded-md border border-border/80 bg-background/90 p-3 font-mono text-xs leading-relaxed text-foreground"> |
|
{jsonPretty} |
|
</pre> |
|
</div> |
|
) |
|
}
|
|
|