import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system"; import { WorkerRelayInterface } from "@snort/worker-relay"; import classNames from "classnames"; import { useCallback, useEffect, useState } from "react"; import { useInView } from "react-intersection-observer"; import { FormattedMessage } from "react-intl"; import { useNavigate } from "react-router-dom"; import { LRUCache } from "typescript-lru-cache"; import { Relay } from "@/Cache"; import NoteHeader from "@/Components/Event/Note/NoteHeader"; import NoteQuote from "@/Components/Event/Note/NoteQuote"; import { NoteText } from "@/Components/Event/Note/NoteText"; import { TranslationInfo } from "@/Components/Event/Note/TranslationInfo"; import { NoteTranslation } from "@/Components/Event/Note/types"; import Username from "@/Components/User/Username"; import useModeration from "@/Hooks/useModeration"; import { chainKey } from "@/Utils/Thread/ChainKey"; import { NoteProps, NotePropsOptions } from "../EventComponent"; import HiddenNote from "../HiddenNote"; import Poll from "../Poll"; import NoteAppHandler from "./NoteAppHandler"; import NoteFooter from "./NoteFooter/NoteFooter"; const defaultOptions = { showHeader: true, showTime: true, showFooter: true, canUnpin: false, canUnbookmark: false, showContextMenu: true, }; const canRenderAsTextNote = [ EventKind.TextNote, EventKind.Polls, EventKind.Photo, EventKind.Video, EventKind.ShortVideo, EventKind.Comment, ]; const translationCache = new LRUCache({ maxSize: 300 }); export function Note(props: NoteProps) { const { data: ev, highlight, options: opt, ignoreModeration = false, className, waitUntilInView } = props; const baseClassName = classNames("note min-h-[110px] flex flex-col gap-4 card", className ?? ""); const { isEventMuted } = useModeration(); const { ref, inView } = useInView({ triggerOnce: true }); const { ref: setSeenAtRef, inView: setSeenAtInView } = useInView({ rootMargin: "0px", threshold: 1 }); const [showTranslation, setShowTranslation] = useState(true); const [translated, setTranslated] = useState(translationCache.get(ev.id)); const cachedSetTranslated = useCallback( (translation: NoteTranslation) => { translationCache.set(ev.id, translation); setTranslated(translation); }, [ev.id], ); useEffect(() => { let timeout: ReturnType; if (setSeenAtInView) { timeout = setTimeout(() => { if (Relay instanceof WorkerRelayInterface) { Relay.setEventMetadata(ev.id, { seen_at: Math.round(Date.now() / 1000) }); } }, 1000); } return () => clearTimeout(timeout); }, [setSeenAtInView]); const optionsMerged = { ...defaultOptions, ...opt }; const goToEvent = useGoToEvent(props, optionsMerged); if (!canRenderAsTextNote.includes(ev.kind)) { return handleNonTextNote(ev); } function content() { if (waitUntilInView && !inView) return null; return ( <> {optionsMerged.showHeader && ( )}
goToEvent(e, ev)}> {translated && } {ev.kind === EventKind.Polls && } {optionsMerged.showFooter && (
)}
); } const noteElement = (
goToEvent(e, ev)} ref={ref}> {content()}
); return !ignoreModeration && isEventMuted(ev) ? {noteElement} : noteElement; } function useGoToEvent(props: NoteProps, options: NotePropsOptions) { const navigate = useNavigate(); return useCallback( (e: React.MouseEvent, eTarget: TaggedNostrEvent) => { if (options?.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; } target = target.parentElement; } e.stopPropagation(); // prevent navigation if selecting text const cellText = document.getSelection(); if (cellText?.type === "Range") { return; } // custom onclick handler if (props.onClick) { props.onClick(eTarget); return; } // link to event const link = NostrLink.fromEvent(eTarget); if (e.metaKey) { window.open(`/${link.encode(CONFIG.eventLinkPrefix)}`, "_blank"); } else { navigate(`/${link.encode(CONFIG.eventLinkPrefix)}`, { state: eTarget }); } }, [navigate, props, options], ); } function Reaction({ ev }: { ev: TaggedNostrEvent }) { const reactedToTag = ev.tags.findLast(tag => tag[0] === "e"); const pTag = ev.tags.findLast(tag => tag[0] === "p"); if (!reactedToTag?.length) { return null; } const link = NostrLink.fromTag(reactedToTag, pTag?.[1]); return (
{}} />
); } function handleNonTextNote(ev: TaggedNostrEvent) { if (ev.kind === EventKind.Reaction) { return ; } else { return ; } }