index.tsx raw

   1  import { usePrimaryPage, useSecondaryPage } from '@/PageManager'
   2  import { ExtendedKind, NSFW_DISPLAY_POLICY, SUPPORTED_KINDS } from '@/constants'
   3  import { getParentStuff, isNsfwEvent } from '@/lib/event'
   4  import { toExternalContent, toNote } from '@/lib/link'
   5  import { useContentPolicy } from '@/providers/ContentPolicyProvider'
   6  import { useDM } from '@/providers/DMProvider'
   7  import { useMuteList } from '@/providers/MuteListProvider'
   8  import { useNostr } from '@/providers/NostrProvider'
   9  import { useScreenSize } from '@/providers/ScreenSizeProvider'
  10  import { Event, kinds } from 'nostr-tools'
  11  import { useMemo, useState } from 'react'
  12  import AudioPlayer from '../AudioPlayer'
  13  import ClientTag from '../ClientTag'
  14  import Content from '../Content'
  15  import FollowingBadge from '../FollowingBadge'
  16  import { FormattedTimestamp } from '../FormattedTimestamp'
  17  import Nip05 from '../Nip05'
  18  import NoteOptions from '../NoteOptions'
  19  import ParentNotePreview from '../ParentNotePreview'
  20  import TrustScoreBadge from '../TrustScoreBadge'
  21  import UserAvatar from '../UserAvatar'
  22  import Username from '../Username'
  23  import { Code, Mail, Type } from 'lucide-react'
  24  import CommunityDefinition from './CommunityDefinition'
  25  import EmojiPack from './EmojiPack'
  26  import FollowPack from './FollowPack'
  27  import GroupMetadata from './GroupMetadata'
  28  import Highlight from './Highlight'
  29  import LiveEvent from './LiveEvent'
  30  import LongFormArticle from './LongFormArticle'
  31  import LongFormArticlePreview from './LongFormArticlePreview'
  32  import MutedNote from './MutedNote'
  33  import NsfwNote from './NsfwNote'
  34  import PictureNote from './PictureNote'
  35  import Poll from './Poll'
  36  import RelayReview from './RelayReview'
  37  import UnknownNote from './UnknownNote'
  38  import VideoNote from './VideoNote'
  39  
  40  export default function Note({
  41    event,
  42    originalNoteId,
  43    size = 'normal',
  44    className,
  45    hideParentNotePreview = false,
  46    showFull = false
  47  }: {
  48    event: Event
  49    originalNoteId?: string
  50    size?: 'normal' | 'small'
  51    className?: string
  52    hideParentNotePreview?: boolean
  53    showFull?: boolean
  54  }) {
  55    const { push } = useSecondaryPage()
  56    const { navigate } = usePrimaryPage()
  57    const { isSmallScreen } = useScreenSize()
  58    const { pubkey } = useNostr()
  59    const { startConversation } = useDM()
  60    const { parentEventId, parentExternalContent } = useMemo(() => {
  61      return getParentStuff(event)
  62    }, [event])
  63    const { nsfwDisplayPolicy, enableMarkdown: globalEnableMarkdown } = useContentPolicy()
  64    const [showNsfw, setShowNsfw] = useState(false)
  65    const { mutePubkeySet } = useMuteList()
  66    const [showMuted, setShowMuted] = useState(false)
  67    const [markdownOverride, setMarkdownOverride] = useState<boolean | null>(null)
  68    const effectiveMarkdown = markdownOverride ?? globalEnableMarkdown
  69  
  70    const handleStartConversation = (e: React.MouseEvent) => {
  71      e.stopPropagation()
  72      startConversation(event.pubkey)
  73      navigate('inbox')
  74    }
  75    const isNsfw = useMemo(
  76      () => (nsfwDisplayPolicy === NSFW_DISPLAY_POLICY.SHOW ? false : isNsfwEvent(event)),
  77      [event, nsfwDisplayPolicy]
  78    )
  79  
  80    let content: React.ReactNode
  81    if (
  82      ![
  83        ...SUPPORTED_KINDS,
  84        kinds.CommunityDefinition,
  85        kinds.LiveEvent,
  86        ExtendedKind.GROUP_METADATA
  87      ].includes(event.kind)
  88    ) {
  89      content = <UnknownNote className="mt-2" event={event} />
  90    } else if (mutePubkeySet.has(event.pubkey) && !showMuted) {
  91      content = <MutedNote show={() => setShowMuted(true)} />
  92    } else if (isNsfw && !showNsfw) {
  93      content = <NsfwNote show={() => setShowNsfw(true)} />
  94    } else if (event.kind === kinds.Highlights) {
  95      content = <Highlight className="mt-2" event={event} />
  96    } else if (event.kind === kinds.LongFormArticle) {
  97      content = showFull ? (
  98        <LongFormArticle className="mt-2" event={event} />
  99      ) : (
 100        <LongFormArticlePreview className="mt-2" event={event} />
 101      )
 102    } else if (event.kind === kinds.LiveEvent) {
 103      content = <LiveEvent className="mt-2" event={event} />
 104    } else if (event.kind === ExtendedKind.GROUP_METADATA) {
 105      content = <GroupMetadata className="mt-2" event={event} originalNoteId={originalNoteId} />
 106    } else if (event.kind === kinds.CommunityDefinition) {
 107      content = <CommunityDefinition className="mt-2" event={event} />
 108    } else if (event.kind === ExtendedKind.POLL) {
 109      content = (
 110        <>
 111          <Content className="mt-2" event={event} enableMarkdown={effectiveMarkdown} />
 112          <Poll className="mt-2" event={event} />
 113        </>
 114      )
 115    } else if (event.kind === ExtendedKind.VOICE || event.kind === ExtendedKind.VOICE_COMMENT) {
 116      content = <AudioPlayer className="mt-2" src={event.content} />
 117    } else if (event.kind === ExtendedKind.PICTURE) {
 118      content = <PictureNote className="mt-2" event={event} />
 119    } else if (
 120      event.kind === ExtendedKind.VIDEO ||
 121      event.kind === ExtendedKind.SHORT_VIDEO ||
 122      event.kind === ExtendedKind.ADDRESSABLE_NORMAL_VIDEO ||
 123      event.kind === ExtendedKind.ADDRESSABLE_SHORT_VIDEO
 124    ) {
 125      content = <VideoNote className="mt-2" event={event} />
 126    } else if (event.kind === ExtendedKind.RELAY_REVIEW) {
 127      content = <RelayReview className="mt-2" event={event} />
 128    } else if (event.kind === kinds.Emojisets) {
 129      content = <EmojiPack className="mt-2" event={event} />
 130    } else if (event.kind === ExtendedKind.FOLLOW_PACK) {
 131      content = <FollowPack className="mt-2" event={event} />
 132    } else {
 133      content = <Content className="mt-2" event={event} enableHighlight enableMarkdown={effectiveMarkdown} />
 134    }
 135  
 136    return (
 137      <div className={className}>
 138        <div className="flex justify-between items-start gap-2">
 139          <div className="flex items-center space-x-2 flex-1">
 140            <UserAvatar userId={event.pubkey} size={size === 'small' ? 'medium' : 'normal'} />
 141            <div className="flex-1 w-0">
 142              <div className="flex gap-2 items-center">
 143                <Username
 144                  userId={event.pubkey}
 145                  className={`font-semibold flex truncate ${size === 'small' ? 'text-sm' : ''}`}
 146                  skeletonClassName={size === 'small' ? 'h-3' : 'h-4'}
 147                />
 148                <FollowingBadge pubkey={event.pubkey} />
 149                <TrustScoreBadge pubkey={event.pubkey} />
 150                <ClientTag event={event} />
 151                {pubkey && pubkey !== event.pubkey && (
 152                  <button
 153                    onClick={handleStartConversation}
 154                    className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
 155                    title="Start conversation"
 156                  >
 157                    <Mail className="size-3.5" />
 158                  </button>
 159                )}
 160              </div>
 161              <div className="flex items-center gap-1 text-sm text-muted-foreground">
 162                <Nip05 pubkey={event.pubkey} append="ยท" />
 163                <FormattedTimestamp
 164                  timestamp={event.created_at}
 165                  className="shrink-0"
 166                  short={isSmallScreen}
 167                />
 168              </div>
 169            </div>
 170          </div>
 171          {size === 'normal' && (
 172            <div className="flex items-center shrink-0">
 173              <button
 174                onClick={(e) => {
 175                  e.stopPropagation()
 176                  setMarkdownOverride((prev) => {
 177                    if (prev === null) return !globalEnableMarkdown
 178                    return null
 179                  })
 180                }}
 181                className={`p-1 rounded hover:bg-accent transition-colors ${
 182                  markdownOverride !== null ? 'text-foreground' : 'text-muted-foreground'
 183                }`}
 184                title={effectiveMarkdown ? 'Show plain text' : 'Show markdown'}
 185              >
 186                {effectiveMarkdown ? <Type className="size-4" /> : <Code className="size-4" />}
 187              </button>
 188              <NoteOptions event={event} className="py-1 [&_svg]:size-5" />
 189            </div>
 190          )}
 191        </div>
 192        {!hideParentNotePreview && (
 193          <ParentNotePreview
 194            eventId={parentEventId}
 195            externalContent={parentExternalContent}
 196            className="mt-2"
 197            onClick={(e) => {
 198              e.stopPropagation()
 199              if (parentExternalContent) {
 200                push(toExternalContent(parentExternalContent))
 201              } else if (parentEventId) {
 202                push(toNote(parentEventId))
 203              }
 204            }}
 205          />
 206        )}
 207        {content}
 208      </div>
 209    )
 210  }
 211