MessageContent.tsx raw

   1  import { useSecondaryPage } from '@/PageManager'
   2  import {
   3    EmbeddedEventParser,
   4    EmbeddedMentionParser,
   5    EmbeddedUrlParser,
   6    parseContent
   7  } from '@/lib/content-parser'
   8  import { toNote, toProfile } from '@/lib/link'
   9  import { truncateUrl } from '@/lib/url'
  10  import { cn } from '@/lib/utils'
  11  import { useMemo } from 'react'
  12  
  13  interface MessageContentProps {
  14    content: string
  15    className?: string
  16    /** If true, links will be styled for dark background (primary-foreground color) */
  17    isOwnMessage?: boolean
  18  }
  19  
  20  /**
  21   * Renders DM message content with linkified URLs and nostr entities.
  22   * - URLs open in new tab
  23   * - nostr:npub/nprofile opens user profile in secondary pane
  24   * - nostr:note1/nevent opens note in secondary pane
  25   */
  26  export default function MessageContent({ content, className, isOwnMessage }: MessageContentProps) {
  27    const { push } = useSecondaryPage()
  28  
  29    const nodes = useMemo(() => {
  30      return parseContent(content, [EmbeddedEventParser, EmbeddedMentionParser, EmbeddedUrlParser])
  31    }, [content])
  32  
  33    const linkClass = cn(
  34      'underline cursor-pointer hover:opacity-80',
  35      isOwnMessage ? 'text-primary-foreground' : 'text-primary'
  36    )
  37  
  38    return (
  39      <span className={cn('whitespace-pre-wrap break-words', className)}>
  40        {nodes.map((node, index) => {
  41          if (node.type === 'text') {
  42            return node.data
  43          }
  44  
  45          // URLs - open in new tab
  46          if (node.type === 'url' || node.type === 'image' || node.type === 'media') {
  47            const url = node.data as string
  48            return (
  49              <a
  50                key={index}
  51                href={url}
  52                target="_blank"
  53                rel="noreferrer"
  54                className={linkClass}
  55                onClick={(e) => e.stopPropagation()}
  56              >
  57                {truncateUrl(url)}
  58              </a>
  59            )
  60          }
  61  
  62          // YouTube and X posts - open in new tab
  63          if (node.type === 'youtube' || node.type === 'x-post') {
  64            const url = node.data as string
  65            return (
  66              <a
  67                key={index}
  68                href={url}
  69                target="_blank"
  70                rel="noreferrer"
  71                className={linkClass}
  72                onClick={(e) => e.stopPropagation()}
  73              >
  74                {truncateUrl(url)}
  75              </a>
  76            )
  77          }
  78  
  79          // nostr: mention (npub/nprofile) - open profile in secondary pane
  80          if (node.type === 'mention') {
  81            const bech32 = (node.data as string).replace('nostr:', '')
  82            return (
  83              <button
  84                key={index}
  85                className={linkClass}
  86                onClick={(e) => {
  87                  e.stopPropagation()
  88                  push(toProfile(bech32))
  89                }}
  90              >
  91                @{bech32.slice(0, 12)}...
  92              </button>
  93            )
  94          }
  95  
  96          // nostr: event (note1/nevent/naddr) - open note in secondary pane
  97          if (node.type === 'event') {
  98            const bech32 = (node.data as string).replace('nostr:', '')
  99            // Determine display based on prefix
 100            const isNote = bech32.startsWith('note1')
 101            const prefix = isNote ? 'note' : bech32.startsWith('nevent') ? 'nevent' : 'naddr'
 102            return (
 103              <button
 104                key={index}
 105                className={linkClass}
 106                onClick={(e) => {
 107                  e.stopPropagation()
 108                  push(toNote(bech32))
 109                }}
 110              >
 111                {prefix}:{bech32.slice(prefix.length, prefix.length + 8)}...
 112              </button>
 113            )
 114          }
 115  
 116          return null
 117        })}
 118      </span>
 119    )
 120  }
 121