Highlight.tsx raw
1 import { Pubkey } from '@/domain'
2 import { useFetchEvent } from '@/hooks'
3 import { createFakeEvent } from '@/lib/event'
4 import { toNote } from '@/lib/link'
5 import { generateBech32IdFromATag, generateBech32IdFromETag } from '@/lib/tag'
6 import { cn } from '@/lib/utils'
7 import { useSecondaryPage } from '@/PageManager'
8 import { Event } from 'nostr-tools'
9 import { useMemo } from 'react'
10 import { useTranslation } from 'react-i18next'
11 import Content from '../Content'
12 import ContentPreview from '../ContentPreview'
13 import ExternalLink from '../ExternalLink'
14 import UserAvatar from '../UserAvatar'
15
16 export default function Highlight({ event, className }: { event: Event; className?: string }) {
17 const comment = useMemo(
18 () => event.tags.find((tag) => tag[0] === 'comment')?.[1],
19 [event]
20 )
21
22 return (
23 <div className={cn('text-wrap break-words whitespace-pre-wrap space-y-4', className)}>
24 {comment && <Content event={createFakeEvent({ content: comment, tags: event.tags })} />}
25 <div className="flex gap-4">
26 <div className="w-1 flex-shrink-0 my-1 bg-primary/60 rounded-md" />
27 <div
28 className="italic whitespace-pre-line"
29 style={{
30 overflowWrap: 'anywhere'
31 }}
32 >
33 {event.content}
34 </div>
35 </div>
36 <HighlightSource event={event} />
37 </div>
38 )
39 }
40
41 function HighlightSource({ event }: { event: Event }) {
42 const { t } = useTranslation()
43 const { push } = useSecondaryPage()
44 const sourceTag = useMemo(() => {
45 let sourceTag: string[] | undefined
46 for (const tag of event.tags) {
47 // Highest priority: 'source' tag
48 if (tag[2] === 'source') {
49 sourceTag = tag
50 break
51 }
52
53 // Give 'e' tags highest priority
54 if (tag[0] === 'e') {
55 sourceTag = tag
56 continue
57 }
58
59 // Give 'a' tags second priority over 'e' tags
60 if (tag[0] === 'a' && (!sourceTag || sourceTag[0] !== 'e')) {
61 sourceTag = tag
62 continue
63 }
64
65 // Give 'r' tags lowest priority
66 if (tag[0] === 'r' && (!sourceTag || sourceTag[0] === 'r')) {
67 sourceTag = tag
68 continue
69 }
70 }
71
72 return sourceTag
73 }, [event])
74 const { event: referenceEvent } = useFetchEvent(
75 sourceTag
76 ? sourceTag[0] === 'e'
77 ? generateBech32IdFromETag(sourceTag)
78 : sourceTag[0] === 'a'
79 ? generateBech32IdFromATag(sourceTag)
80 : undefined
81 : undefined
82 )
83 const referenceEventId = useMemo(() => {
84 if (!sourceTag || sourceTag[0] === 'r') return
85 if (sourceTag[0] === 'e') {
86 return sourceTag[1]
87 }
88 if (sourceTag[0] === 'a') {
89 return generateBech32IdFromATag(sourceTag)
90 }
91 }, [sourceTag])
92 const pubkey = useMemo(() => {
93 if (referenceEvent) {
94 return referenceEvent.pubkey
95 }
96 if (sourceTag && sourceTag[0] === 'a') {
97 const [, pubkey] = sourceTag[1].split(':')
98 if (Pubkey.isValidHex(pubkey)) {
99 return pubkey
100 }
101 }
102 }, [sourceTag, referenceEvent])
103
104 if (!sourceTag) {
105 return null
106 }
107
108 if (sourceTag[0] === 'r') {
109 return (
110 <div className="truncate text-muted-foreground">
111 {t('From')}{' '}
112 <ExternalLink
113 url={sourceTag[1]}
114 className="underline italic text-muted-foreground hover:text-foreground"
115 />
116 </div>
117 )
118 }
119
120 return (
121 <div className="flex items-center gap-2 text-muted-foreground">
122 <div className="shrink-0">{t('From')}</div>
123 {pubkey && <UserAvatar userId={pubkey} size="xSmall" className="cursor-pointer" />}
124 {referenceEventId && (
125 <div
126 className="truncate underline pointer-events-auto cursor-pointer hover:text-foreground"
127 onClick={(e) => {
128 e.stopPropagation()
129 push(toNote(referenceEvent ?? referenceEventId))
130 }}
131 >
132 {referenceEvent ? <ContentPreview event={referenceEvent} /> : referenceEventId}
133 </div>
134 )}
135 </div>
136 )
137 }
138