import { useSmartNoteNavigationOptional } from '@/PageManager' import { ExtendedKind } from '@/constants' import { isRenderableNoteKind } from '@/lib/note-renderable-kinds' import { getHttpUrlFromITags, getParentBech32Id, isNip18RepostKind, isNip25ReactionKind, isNsfwEvent } from '@/lib/event' import { getCachedThreadContextEvents } from '@/lib/navigation-related-events' import { toNote } from '@/lib/link' import { cn } from '@/lib/utils' import { DISCUSSION_DOWNVOTE_DISPLAY, DISCUSSION_UPVOTE_DISPLAY } from '@/lib/discussion-votes' import { notificationReactionSummaryKey, useNotificationReactionDisplay } from '@/hooks/useNotificationReactionDisplay' import logger from '@/lib/logger' import client from '@/services/client.service' import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' import { useMuteListOptional } from '@/contexts/mute-list-context' import { muteSetHas } from '@/lib/mute-set' import { useScreenSizeOptional } from '@/providers/ScreenSizeProvider' import type { HighlightData } from '@/components/PostEditor/HighlightEditor' import { Event, kinds } from 'nostr-tools' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { getWebExternalReactionTargetUrl, isRssThreadSyntheticParentEvent } from '@/lib/rss-article' import { CreateHighlightContext } from './CreateHighlightContext' import SelectionHighlightTrigger from './SelectionHighlightTrigger' import AudioPlayer from '../AudioPlayer' import WebPreview from '../WebPreview' import ClientTag from '../ClientTag' import { FormattedTimestamp } from '../FormattedTimestamp' import Nip05 from '../Nip05' import NoteOptions from '../NoteOptions' import ParentNotePreview from '../ParentNotePreview' import UserAvatar from '../UserAvatar' import Username from '../Username' import { MessageSquare } from 'lucide-react' import CommunityDefinition from './CommunityDefinition' import GroupMetadata from './GroupMetadata' import Highlight from './Highlight' import IValue from './IValue' import LiveEvent from './LiveEvent' import LongFormArticlePreview from './LongFormArticlePreview' import MarkdownArticle from './MarkdownArticle/MarkdownArticle' import AsciidocArticle from './AsciidocArticle/AsciidocArticle' import PublicationCard from './PublicationCard' import PublicationIndex from './PublicationIndex/PublicationIndex' import WikiCard from './WikiCard' import MutedNote from './MutedNote' import NsfwNote from './NsfwNote' import PictureNote from './PictureNote' import Poll from './Poll' import ZapPoll from './ZapPoll' import NotificationEventCard from './NotificationEventCard' import ReactionEmojiDisplay from './ReactionEmojiDisplay' import UnknownNote from './UnknownNote' import NoteKindLabel from './NoteKindLabel' import { Skeleton } from '@/components/ui/skeleton' import VideoNote from './VideoNote' import RelayReview from './RelayReview' import Zap from './Zap' import CitationCard from '@/components/CitationCard' import FollowPackPreview from '../ContentPreview/FollowPackPreview' import CalendarEventContent from '../CalendarEventContent' import GitRepublicEventCard from './GitRepublicEventCard' export default function Note({ event, originalNoteId, size = 'normal', className, hideParentNotePreview = false, showFull = false, disableClick = false, fullCalendarInvite, zapPollVoteHighlightOption }: { event: Event originalNoteId?: string size?: 'normal' | 'small' className?: string hideParentNotePreview?: boolean showFull?: boolean disableClick?: boolean /** When viewing a kind-24 invite, use this to replace the embedded calendar with the full card (RSVP) in content */ fullCalendarInvite?: { event: Event; naddr: string } /** Profile: highlight option when this row is from a zap vote receipt. */ zapPollVoteHighlightOption?: number }) { const { t } = useTranslation() const { navigateToNote } = useSmartNoteNavigationOptional() const screenSize = useScreenSizeOptional() const isSmallScreen = screenSize?.isSmallScreen ?? false const parentEventId = useMemo( () => (hideParentNotePreview ? undefined : getParentBech32Id(event)), [event, hideParentNotePreview] ) const contentPolicy = useContentPolicyOptional() const defaultShowNsfw = contentPolicy?.defaultShowNsfw ?? true const [showNsfw, setShowNsfw] = useState(false) const muteList = useMuteListOptional() const mutePubkeySet = muteList?.mutePubkeySet ?? new Set() const [showMuted, setShowMuted] = useState(false) const [highlightData, setHighlightData] = useState(undefined) const [highlightDefaultContent, setHighlightDefaultContent] = useState('') const [postEditorOpen, setPostEditorOpen] = useState(false) const [publicMessageTo, setPublicMessageTo] = useState(null) const [callInviteContent, setCallInviteContent] = useState(null) const reactionDisplay = useNotificationReactionDisplay(event) const webReactionParentUrl = useMemo( () => event.kind === ExtendedKind.EXTERNAL_REACTION ? getWebExternalReactionTargetUrl(event) : undefined, [event] ) const openHighlight = useCallback((data: HighlightData, eventContent?: string) => { setHighlightData(data) setHighlightDefaultContent(eventContent ?? '') setPublicMessageTo(null) setCallInviteContent(null) setPostEditorOpen(true) }, []) const openPublicMessage = useCallback((pubkey: string) => { setPublicMessageTo(pubkey) setCallInviteContent(null) setPostEditorOpen(true) }, []) const openCallInvite = useCallback((url: string) => { setCallInviteContent(url) setPublicMessageTo(null) setHighlightData(undefined) setHighlightDefaultContent('') setPostEditorOpen(true) }, []) const isHighlightableKind = event.kind === kinds.ShortTextNote || event.kind === kinds.LongFormArticle || event.kind === ExtendedKind.WIKI_ARTICLE || event.kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN || event.kind === ExtendedKind.PUBLICATION || event.kind === ExtendedKind.PUBLICATION_CONTENT || event.kind === ExtendedKind.DISCUSSION || event.kind === ExtendedKind.CALENDAR_EVENT_TIME || event.kind === ExtendedKind.CALENDAR_EVENT_DATE || event.kind === ExtendedKind.COMMENT let content: React.ReactNode if (!isRenderableNoteKind(event.kind)) { logger.debug('Note component - rendering UnknownNote for unsupported kind:', event.kind) content = } else if (muteSetHas(mutePubkeySet, event.pubkey) && !showMuted) { content = setShowMuted(true)} /> } else if (!defaultShowNsfw && isNsfwEvent(event) && !showNsfw) { content = setShowNsfw(true)} /> } else if (isNip25ReactionKind(event.kind)) { content = null } else if (isNip18RepostKind(event.kind) || event.kind === ExtendedKind.POLL_RESPONSE) { content = } else if (event.kind === kinds.Highlights) { // Try to render the Highlight component with error boundary try { content = } catch (error) { logger.error('Note component - Error rendering Highlight component:', error) content =
HIGHLIGHT ERROR:
Error: {String(error)}
Content: {event.content}
Context: {event.tags.find(tag => tag[0] === 'context')?.[1] || 'No context found'}
} } else if (event.kind === ExtendedKind.WIKI_ARTICLE) { content = showFull ? ( ) : ( ) } else if (event.kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN) { content = showFull ? ( ) : ( ) } else if (event.kind === ExtendedKind.PUBLICATION) { content = showFull ? ( ) : ( ) } else if (event.kind === ExtendedKind.PUBLICATION_CONTENT) { content = showFull ? ( ) : ( ) } else if (event.kind === kinds.LongFormArticle) { content = showFull ? ( ) : ( ) } else if (event.kind === kinds.LiveEvent) { content = } else if (event.kind === ExtendedKind.GROUP_METADATA) { content = } else if (event.kind === kinds.CommunityDefinition) { content = } else if (event.kind === ExtendedKind.DISCUSSION) { const titleTag = event.tags.find(tag => tag[0] === 'title') const title = titleTag?.[1] || 'Untitled Discussion' content = ( <>

{title}

) } else if ( event.kind === ExtendedKind.CITATION_INTERNAL || event.kind === ExtendedKind.CITATION_EXTERNAL || event.kind === ExtendedKind.CITATION_HARDCOPY || event.kind === ExtendedKind.CITATION_PROMPT ) { content = } else if (event.kind === ExtendedKind.POLL) { content = ( <> ) } else if (event.kind === ExtendedKind.ZAP_POLL) { content = ( <> ) } else if (event.kind === ExtendedKind.VOICE) { content = } else if (event.kind === ExtendedKind.VOICE_COMMENT) { const voiceArticleUrl = getHttpUrlFromITags(event) content = ( <> {voiceArticleUrl && (
)} ) } else if (event.kind === ExtendedKind.PICTURE) { content = } else if (event.kind === ExtendedKind.VIDEO || event.kind === ExtendedKind.SHORT_VIDEO) { content = } else if (event.kind === ExtendedKind.RELAY_REVIEW) { content = } else if (event.kind === ExtendedKind.CALENDAR_EVENT_TIME || event.kind === ExtendedKind.CALENDAR_EVENT_DATE) { content = } else if (event.kind === ExtendedKind.PUBLIC_MESSAGE) { content = ( ) } else if (event.kind === ExtendedKind.ZAP_REQUEST || event.kind === ExtendedKind.ZAP_RECEIPT) { content = } else if (event.kind === ExtendedKind.FOLLOW_PACK) { content = } else if ( event.kind === ExtendedKind.GIT_REPO_ANNOUNCEMENT || event.kind === ExtendedKind.GIT_ISSUE || event.kind === ExtendedKind.GIT_RELEASE ) { content = } else if (event.kind === kinds.ShortTextNote || event.kind === ExtendedKind.COMMENT) { // Plain text notes use MarkdownArticle for proper markdown rendering content = } else { // Use MarkdownArticle for all other kinds content = } const isSyntheticRssParent = isRssThreadSyntheticParentEvent(event) const wrappedContent = isHighlightableKind ? ( {content} ) : ( content ) return (
{ // Don't navigate if clicking on interactive elements const target = e.target as HTMLElement if (target.closest('button') || target.closest('[role="button"]') || target.closest('a') || target.closest('[data-embedded-note]') || target.closest('[data-parent-note-preview]') || target.closest('[data-user-avatar]') || target.closest('[data-username]')) { return } e.stopPropagation() client.addEventToCache(event) navigateToNote(toNote(event), event, getCachedThreadContextEvents(event)) }} >
{isNip25ReactionKind(event.kind) ? (
{reactionDisplay.status === 'pending' ? ( ) : reactionDisplay.status === 'vote_up' ? ( {DISCUSSION_UPVOTE_DISPLAY} ) : reactionDisplay.status === 'vote_down' ? ( {DISCUSSION_DOWNVOTE_DISPLAY} ) : ( )}
{t(notificationReactionSummaryKey(reactionDisplay))}
) : isSyntheticRssParent ? ( <>
{t('Jumble Imwald synthetic event')}
) : ( <>
)}
{event.kind === ExtendedKind.DISCUSSION && ( )} {(size === 'normal' || event.kind === ExtendedKind.ZAP_REQUEST || event.kind === ExtendedKind.ZAP_RECEIPT) && ( { setPostEditorOpen(false) setHighlightData(undefined) setHighlightDefaultContent('') setPublicMessageTo(null) setCallInviteContent(null) }} onOpenPublicMessage={openPublicMessage} initialPublicMessageTo={publicMessageTo} onOpenCallInvite={openCallInvite} initialDefaultContent={callInviteContent} /> )}
{webReactionParentUrl ? (
) : parentEventId ? ( { e.stopPropagation() const parentEv = client.peekSessionCachedEvent(parentEventId) navigateToNote( toNote(parentEventId), parentEv, parentEv ? getCachedThreadContextEvents(parentEv) : undefined ) }} /> ) : null} {wrappedContent}
) }