import { Skeleton } from '@/components/ui/skeleton' import { ExtendedKind, isNip71StyleVideoKind } from '@/constants' import { notificationReactionSummaryKey, useNotificationReactionDisplay } from '@/hooks/useNotificationReactionDisplay' import { isMentioningMutedUsers, isNip18RepostKind, isNip25ReactionKind } from '@/lib/event' import { DISCUSSION_DOWNVOTE_DISPLAY, DISCUSSION_UPVOTE_DISPLAY } from '@/lib/discussion-votes' import { getWebBookmarkArticleUrl } from '@/lib/rss-article' import { cn } from '@/lib/utils' import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' import { useMuteListOptional } from '@/contexts/mute-list-context' import { muteSetHas } from '@/lib/mute-set' import { mergeTranslatedNote, useNoteTranslation } from '@/lib/note-translation-display' import { Event, kinds } from 'nostr-tools' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import CommunityDefinitionPreview from './CommunityDefinitionPreview' import GroupMetadataPreview from './GroupMetadataPreview' import HighlightPreview from './HighlightPreview' import LiveEventPreview from './LiveEventPreview' import LongFormArticlePreview from './LongFormArticlePreview' import NormalContentPreview from './NormalContentPreview' import PictureNotePreview from './PictureNotePreview' import PollPreview from './PollPreview' import VideoNotePreview from './VideoNotePreview' import ZapPreview from './ZapPreview' import DiscussionNote from '../DiscussionNote' import ApplicationHandlerInfo from '../ApplicationHandlerInfo' import ApplicationHandlerRecommendation from '../ApplicationHandlerRecommendation' import FollowPackPreview from './FollowPackPreview' import ReactionEmojiDisplay from '../Note/ReactionEmojiDisplay' import NoteKindLabel from '../Note/NoteKindLabel' import Zap from '../Note/Zap' import GitRepublicEventCard from '../Note/GitRepublicEventCard' /** Inert event so hooks can run before `event` is defined. */ const CONTENT_PREVIEW_HOOK_PLACEHOLDER = { kind: kinds.ShortTextNote, id: '', pubkey: '', content: '', tags: [], created_at: 0, sig: '' } as Event const PARENT_REPLY_POLL_BLURB_MAX = 150 function parentReplyPollQuestionBlurb(content: string): string { const normalized = content.trim().replace(/\s+/g, ' ') if (normalized.length <= PARENT_REPLY_POLL_BLURB_MAX) return normalized return `${normalized.slice(0, PARENT_REPLY_POLL_BLURB_MAX)}…` } /** Keep spacing/margins on the outer wrapper; put line-clamp on the preview body so it still clamps text. */ function splitPreviewLayoutClasses(className?: string) { if (!className?.trim()) return { outer: undefined, body: undefined } const tokens = className.trim().split(/\s+/) const body: string[] = [] const outer: string[] = [] for (const tok of tokens) { if (tok.startsWith('line-clamp')) body.push(tok) else outer.push(tok) } return { outer: outer.length ? outer.join(' ') : undefined, body: body.length ? body.join(' ') : undefined } } export default function ContentPreview({ event, className, /** Inline parent lines (e.g. reply thread): zap receipts match compact thread styling. */ previewDensity, /** Reply-to-parent strip: polls show a short question snippet instead of full poll UI. */ forParentReplyBlurb = false }: { event?: Event className?: string previewDensity?: 'default' | 'compact' forParentReplyBlurb?: boolean }) { const { t } = useTranslation() const noteTr = useNoteTranslation(event?.id ?? '') const reactionDisplay = useNotificationReactionDisplay(event ?? CONTENT_PREVIEW_HOOK_PLACEHOLDER) const muteList = useMuteListOptional() const mutePubkeySet = muteList?.mutePubkeySet ?? new Set() const contentPolicy = useContentPolicyOptional() const hideContentMentioningMutedUsers = contentPolicy?.hideContentMentioningMutedUsers ?? false const isMuted = useMemo( () => (event ? muteSetHas(mutePubkeySet, event.pubkey) : false), [mutePubkeySet, event] ) const isMentioningMuted = useMemo( () => hideContentMentioningMutedUsers && event ? isMentioningMutedUsers(event, mutePubkeySet) : false, [event, mutePubkeySet] ) if (!event) { return
{`[${t('Note not found')}]`}
} if (isMuted) { return (
[{t('This user has been muted')}]
) } if (isMentioningMuted) { return (
[{t('This note mentions a user you muted')}]
) } const previewEvent = mergeTranslatedNote(event, noteTr) const { outer: previewOuter, body: previewBody } = splitPreviewLayoutClasses(className) const withKindRow = (node: React.ReactNode) => (
{node}
) if ( [ kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT, ExtendedKind.RELAY_REVIEW, ExtendedKind.PUBLIC_MESSAGE ].includes(event.kind) ) { return withKindRow() } if (event.kind === ExtendedKind.DISCUSSION) { return (
) } if (event.kind === kinds.Highlights) { return withKindRow() } if (event.kind === ExtendedKind.WEB_BOOKMARK) { const href = getWebBookmarkArticleUrl(previewEvent) const title = previewEvent.tags.find((t) => t[0] === 'title')?.[1]?.trim() const line = title?.trim() || href?.trim() || t('Web bookmark') return withKindRow(
{line}
) } if (event.kind === ExtendedKind.POLL) { if (forParentReplyBlurb) { const snippet = parentReplyPollQuestionBlurb(previewEvent.content ?? '') return (
{snippet || t('Poll')}
) } return withKindRow() } if (event.kind === kinds.LongFormArticle) { return withKindRow() } if (isNip71StyleVideoKind(event.kind)) { return withKindRow() } if (event.kind === ExtendedKind.PICTURE) { return withKindRow() } if (event.kind === ExtendedKind.GROUP_METADATA) { return withKindRow() } if (event.kind === kinds.CommunityDefinition) { return withKindRow() } if (event.kind === kinds.LiveEvent) { return withKindRow() } if (event.kind === ExtendedKind.ZAP_REQUEST) { return withKindRow() } if (event.kind === ExtendedKind.ZAP_RECEIPT || event.kind === kinds.Zap) { if (previewDensity === 'compact') { return (
) } return withKindRow() } if (event.kind === ExtendedKind.APPLICATION_HANDLER_INFO) { return withKindRow() } if (event.kind === ExtendedKind.APPLICATION_HANDLER_RECOMMENDATION) { return withKindRow() } if (event.kind === ExtendedKind.FOLLOW_PACK) { return withKindRow() } if ( event.kind === ExtendedKind.GIT_REPO_ANNOUNCEMENT || event.kind === ExtendedKind.GIT_ISSUE || event.kind === ExtendedKind.GIT_RELEASE ) { return withKindRow() } if (isNip25ReactionKind(event.kind)) { return withKindRow(
{reactionDisplay.status === 'pending' ? ( ) : reactionDisplay.status === 'vote_up' ? ( {DISCUSSION_UPVOTE_DISPLAY} ) : reactionDisplay.status === 'vote_down' ? ( {DISCUSSION_DOWNVOTE_DISPLAY} ) : ( )} {t(notificationReactionSummaryKey(reactionDisplay))}
) } if (isNip18RepostKind(event.kind)) { return withKindRow(
{t('Notification boost summary')}
) } if (event.kind === ExtendedKind.POLL_RESPONSE) { return withKindRow(
{t('Notification poll vote summary')}
) } return withKindRow(
[{t('Cannot handle event of kind k', { k: previewEvent.kind })}]
) }