ChatContent.tsx raw

   1  import {
   2    EmbeddedEventParser,
   3    EmbeddedMentionParser,
   4    EmbeddedUrlParser,
   5    EmbeddedHashtagParser,
   6    EmbeddedEmojiParser,
   7    parseContent
   8  } from '@/lib/content-parser'
   9  import { EmbeddedMention, EmbeddedHashtag } from '../Embedded'
  10  import { SecondaryPageLink } from '@/PageManager'
  11  import { toNote } from '@/lib/link'
  12  import { truncateUrl } from '@/lib/url'
  13  import { getEmojiInfosFromEmojiTags } from '@/lib/tag'
  14  import Emoji from '../Emoji'
  15  import { useMemo } from 'react'
  16  import { Event } from 'nostr-tools'
  17  
  18  /**
  19   * Lightweight inline content renderer for NIRC chat messages.
  20   * Reuses the same parseContent pipeline as the full Content component
  21   * but renders everything inline to fit the IRC-style monospace layout.
  22   */
  23  export default function ChatContent({ content, event }: { content: string; event?: Event }) {
  24    const { nodes, emojiInfos } = useMemo(() => {
  25      if (!content) return { nodes: [], emojiInfos: [] }
  26      const nodes = parseContent(content, [
  27        EmbeddedEventParser,
  28        EmbeddedMentionParser,
  29        EmbeddedUrlParser,
  30        EmbeddedHashtagParser,
  31        EmbeddedEmojiParser
  32      ])
  33      const emojiInfos = getEmojiInfosFromEmojiTags(event?.tags)
  34      return { nodes, emojiInfos }
  35    }, [content, event])
  36  
  37    if (!nodes || nodes.length === 0) return null
  38  
  39    return (
  40      <span className="break-words whitespace-pre-wrap min-w-0">
  41        {nodes.map((node, i) => {
  42          if (node.type === 'text') {
  43            return <span key={i}>{node.data}</span>
  44          }
  45          if (node.type === 'mention') {
  46            const userId = (node.data as string).split(':')[1]
  47            if (!userId) return <span key={i}>{node.data as string}</span>
  48            return <EmbeddedMention key={i} userId={userId} className="inline" />
  49          }
  50          if (node.type === 'url') {
  51            return (
  52              <a
  53                key={i}
  54                href={node.data as string}
  55                target="_blank"
  56                rel="noopener noreferrer"
  57                className="text-primary hover:underline"
  58                onClick={(e) => e.stopPropagation()}
  59              >
  60                {truncateUrl(node.data as string)}
  61              </a>
  62            )
  63          }
  64          if (node.type === 'image' || node.type === 'images') {
  65            const url = Array.isArray(node.data) ? node.data[0] : node.data
  66            return (
  67              <a
  68                key={i}
  69                href={url}
  70                target="_blank"
  71                rel="noopener noreferrer"
  72                className="text-primary hover:underline"
  73                onClick={(e) => e.stopPropagation()}
  74              >
  75                [image]
  76              </a>
  77            )
  78          }
  79          if (node.type === 'media') {
  80            return (
  81              <a
  82                key={i}
  83                href={node.data as string}
  84                target="_blank"
  85                rel="noopener noreferrer"
  86                className="text-primary hover:underline"
  87                onClick={(e) => e.stopPropagation()}
  88              >
  89                [media]
  90              </a>
  91            )
  92          }
  93          if (node.type === 'event') {
  94            const id = (node.data as string).split(':')[1]
  95            if (!id) return <span key={i}>{node.data as string}</span>
  96            return (
  97              <SecondaryPageLink
  98                key={i}
  99                to={toNote(id)}
 100                className="text-primary hover:underline"
 101                onClick={(e) => e.stopPropagation()}
 102              >
 103                [note]
 104              </SecondaryPageLink>
 105            )
 106          }
 107          if (node.type === 'hashtag') {
 108            return <EmbeddedHashtag key={i} hashtag={node.data as string} />
 109          }
 110          if (node.type === 'emoji') {
 111            const shortcode = (node.data as string).split(':')[1]
 112            const emoji = emojiInfos.find((e) => e.shortcode === shortcode)
 113            if (!emoji) return <span key={i}>{node.data as string}</span>
 114            return <Emoji key={i} emoji={emoji} classNames={{ img: 'size-4 inline' }} />
 115          }
 116          if (node.type === 'youtube' || node.type === 'x-post') {
 117            return (
 118              <a
 119                key={i}
 120                href={node.data as string}
 121                target="_blank"
 122                rel="noopener noreferrer"
 123                className="text-primary hover:underline"
 124                onClick={(e) => e.stopPropagation()}
 125              >
 126                {truncateUrl(node.data as string)}
 127              </a>
 128            )
 129          }
 130          if (node.type === 'invoice') {
 131            return (
 132              <span key={i} className="text-primary">
 133                [ln-invoice]
 134              </span>
 135            )
 136          }
 137          return null
 138        })}
 139      </span>
 140    )
 141  }
 142