import { Link, useNavigate } from "react-router-dom"; import React, { ReactNode, useMemo, useState } from "react"; import { useInView } from "react-intersection-observer"; import { FormattedMessage, useIntl } from "react-intl"; import classNames from "classnames"; import { EventExt, EventKind, HexKey, NostrLink, NostrPrefix, TaggedNostrEvent } from "@snort/system"; import { useEventReactions } from "@snort/system-react"; import { findTag, hexToBech32 } from "@/Utils"; import useModeration from "@/Hooks/useModeration"; import useLogin from "@/Hooks/useLogin"; import useEventPublisher from "@/Hooks/useEventPublisher"; import { NoteContextMenu, NoteTranslation } from "./NoteContextMenu"; import { UserCache } from "@/Cache"; import messages from "../messages"; import { setBookmarked, setPinned } from "@/Utils/Login"; import Text from "../Text/Text"; import Reveal from "./Reveal"; import Poll from "./Poll"; import ProfileImage from "../User/ProfileImage"; import Icon from "@/Components/Icons/Icon"; import NoteTime from "./NoteTime"; import NoteFooter from "./NoteFooter"; import Reactions from "./Reactions"; import HiddenNote from "./HiddenNote"; import { NoteProps } from "./Note"; import { chainKey } from "@/Hooks/useThreadContext"; import { ProfileLink } from "@/Components/User/ProfileLink"; import DisplayName from "@/Components/User/DisplayName"; const TEXT_TRUNCATE_LENGTH = 400; export function NoteInner(props: NoteProps) { const { data: ev, related, highlight, options: opt, ignoreModeration = false, className, waitUntilInView } = props; const baseClassName = classNames("note min-h-[110px] flex flex-col gap-4 card", className); const navigate = useNavigate(); const [showReactions, setShowReactions] = useState(false); const { isEventMuted } = useModeration(); const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "2000px" }); const { reactions, reposts, deletions, zaps } = useEventReactions(NostrLink.fromEvent(ev), related); const login = useLogin(); const { pinned, bookmarked } = useLogin(); const { publisher, system } = useEventPublisher(); const [translated, setTranslated] = useState(); const [showTranslation, setShowTranslation] = useState(true); const { formatMessage } = useIntl(); const [showMore, setShowMore] = useState(false); const totalReactions = reactions.positive.length + reactions.negative.length + reposts.length + zaps.length; const options = { showHeader: true, showTime: true, showFooter: true, canUnpin: false, canUnbookmark: false, showContextMenu: true, ...opt, }; async function unpin(id: HexKey) { if (options.canUnpin && publisher) { if (window.confirm(formatMessage(messages.ConfirmUnpin))) { const es = pinned.item.filter(e => e !== id); const ev = await publisher.pinned(es.map(a => new NostrLink(NostrPrefix.Note, a))); system.BroadcastEvent(ev); setPinned(login, es, ev.created_at * 1000); } } } async function unbookmark(id: HexKey) { if (options.canUnbookmark && publisher) { if (window.confirm(formatMessage(messages.ConfirmUnbookmark))) { const es = bookmarked.item.filter(e => e !== id); const ev = await publisher.pinned(es.map(a => new NostrLink(NostrPrefix.Note, a))); system.BroadcastEvent(ev); setBookmarked(login, es, ev.created_at * 1000); } } } const ToggleShowMore = () => ( { e.preventDefault(); e.stopPropagation(); setShowMore(!showMore); }}> {showMore ? ( ) : ( )} ); const innerContent = useMemo(() => { const body = translated && showTranslation ? translated.text : ev?.content ?? ""; const id = translated && showTranslation ? `${ev.id}-translated` : ev.id; const shouldTruncate = opt?.truncate && body.length > TEXT_TRUNCATE_LENGTH; return ( <> {shouldTruncate && showMore && } {shouldTruncate && !showMore && } ); }, [ showMore, ev, translated, showTranslation, props.searchedValue, props.depth, options.showMedia, props.options?.showMediaSpotlight, opt?.truncate, TEXT_TRUNCATE_LENGTH, ]); const transformBody = () => { if (deletions?.length > 0) { return ( ); } if (!login.appData.item.showContentWarningPosts) { const contentWarning = ev.tags.find(a => a[0] === "content-warning"); if (contentWarning) { return ( {c}, }} /> {contentWarning[1] && ( <>   {c}, reason: contentWarning[1], }} /> )} . .{" "} }> {innerContent} ); } } return innerContent; }; function goToEvent(e: React.MouseEvent, eTarget: TaggedNostrEvent) { if (opt?.canClick === false) { return; } let target = e.target as HTMLElement | null; while (target) { if ( target.tagName === "A" || target.tagName === "BUTTON" || target.classList.contains("reaction-pill") || target.classList.contains("szh-menu-container") ) { return; // is there a better way to do this? } target = target.parentElement; } e.stopPropagation(); if (props.onClick) { props.onClick(eTarget); return; } const link = NostrLink.fromEvent(eTarget); // detect cmd key and open in new tab if (e.metaKey) { window.open(`/${link.encode(CONFIG.eventLinkPrefix)}`, "_blank"); } else { navigate(`/${link.encode(CONFIG.eventLinkPrefix)}`, { state: eTarget, }); } } function replyTag() { const thread = EventExt.extractThread(ev); if (thread === undefined) { return undefined; } const maxMentions = 2; const replyTo = thread?.replyTo ?? thread?.root; const replyLink = replyTo ? NostrLink.fromTag( [replyTo.key, replyTo.value ?? "", replyTo.relay ?? "", replyTo.marker ?? ""].filter(a => a.length > 0), ) : undefined; const mentions: { pk: string; name: string; link: ReactNode }[] = []; for (const pk of thread?.pubKeys ?? []) { const u = UserCache.getFromCache(pk); const npub = hexToBech32(NostrPrefix.PublicKey, pk); const shortNpub = npub.substring(0, 12); mentions.push({ pk, name: u?.name ?? shortNpub, link: ( {" "} ), }); } mentions.sort(a => (a.name.startsWith(NostrPrefix.PublicKey) ? 1 : -1)); const othersLength = mentions.length - maxMentions; const renderMention = (m: { link: React.ReactNode; pk: string; name: string }, idx: number) => { return ( {idx > 0 && ", "} {m.link} ); }; const pubMentions = mentions.length > maxMentions ? mentions?.slice(0, maxMentions).map(renderMention) : mentions?.map(renderMention); const others = mentions.length > maxMentions ? formatMessage(messages.Others, { n: othersLength }) : ""; const link = replyLink?.encode(CONFIG.eventLinkPrefix); return (
re:  {(mentions?.length ?? 0) > 0 ? ( <> {pubMentions} {others} ) : ( replyLink && {link?.substring(0, 12)} )}
); } const canRenderAsTextNote = [EventKind.TextNote, EventKind.Polls]; if (!canRenderAsTextNote.includes(ev.kind)) { const alt = findTag(ev, "alt"); if (alt) { return (
); } else { return ( <>

{JSON.stringify(ev, undefined, "  ")}
); } } function translation() { if (translated && translated.confidence > 0.5) { return ( <> { e.stopPropagation(); setShowTranslation(s => !s); }}> ); } else if (translated) { return (

); } } function pollOptions() { if (ev.kind !== EventKind.Polls) return; return ; } function content() { if (waitUntilInView && !inView) return undefined; return ( <> {options.showHeader && (
{props.context} {(options.showTime || options.showBookmarked) && ( <> {options.showBookmarked && (
unbookmark(ev.id)}>
)} {!options.showBookmarked && } )} {options.showPinned && (
unpin(ev.id)}>
)} {options.showContextMenu && ( {}} onTranslated={t => setTranslated(t)} setShowReactions={setShowReactions} /> )}
)}
goToEvent(e, ev, true)}> {transformBody()} {translation()} {pollOptions()} {options.showReactionsLink && ( setShowReactions(true)}> )}
{options.showFooter && ( )} ); } const note = (
goToEvent(e, ev)} ref={ref}> {content()}
); return !ignoreModeration && isEventMuted(ev) ? {note} : note; }