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