import "./Thread.css"; import { useMemo, useState, ReactNode, useContext } from "react"; import { useIntl } from "react-intl"; import { useNavigate, useParams } from "react-router-dom"; import { TaggedNostrEvent, u256, NostrPrefix, EventExt, parseNostrLink, NostrLink } from "@snort/system"; import classNames from "classnames"; import { getAllLinkReactions, getLinkReactions } from "@/SnortUtils"; import BackButton from "@/Element/Button/BackButton"; import Note from "@/Element/Event/Note"; import NoteGhost from "@/Element/Event/NoteGhost"; import Collapsed from "@/Element/Collapsed"; import { ThreadContext, ThreadContextWrapper, chainKey } from "@/Hooks/useThreadContext"; import messages from "../messages"; interface DividerProps { variant?: "regular" | "small"; } const Divider = ({ variant = "regular" }: DividerProps) => { const className = variant === "small" ? "divider divider-small" : "divider"; return (
); }; interface SubthreadProps { isLastSubthread?: boolean; active: u256; notes: readonly TaggedNostrEvent[]; related: readonly TaggedNostrEvent[]; chains: Map>; onNavigate: (e: TaggedNostrEvent) => void; } const Subthread = ({ active, notes, related, chains, onNavigate }: SubthreadProps) => { const renderSubthread = (a: TaggedNostrEvent, idx: number) => { const isLastSubthread = idx === notes.length - 1; const replies = getReplies(a.id, chains); return ( <>
0 ? "subthread-multi" : ""}`}>
{replies.length > 0 && ( )} ); }; return
{notes.map(renderSubthread)}
; }; interface ThreadNoteProps extends Omit { note: TaggedNostrEvent; isLast: boolean; } const ThreadNote = ({ active, note, isLast, isLastSubthread, related, chains, onNavigate }: ThreadNoteProps) => { const { formatMessage } = useIntl(); const replies = getReplies(note.id, chains); const activeInReplies = replies.map(r => r.id).includes(active); const [collapsed, setCollapsed] = useState(!activeInReplies); const hasMultipleNotes = replies.length > 1; const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes; const className = classNames( "subthread-container", isLast && collapsed ? "subthread-last" : "subthread-multi subthread-mid", ); return ( <>
{replies.length > 0 && ( )} ); }; const TierTwo = ({ active, isLastSubthread, notes, related, chains, onNavigate }: SubthreadProps) => { const [first, ...rest] = notes; return ( <> {rest.map((r: TaggedNostrEvent, idx: number) => { const lastReply = idx === rest.length - 1; return ( ); })} ); }; const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate }: SubthreadProps) => { const [first, ...rest] = notes; const replies = getReplies(first.id, chains); const hasMultipleNotes = rest.length > 0 || replies.length > 0; const isLast = replies.length === 0 && rest.length === 0; return ( <>
{replies.length > 0 && ( )} {rest.map((r: TaggedNostrEvent, idx: number) => { const lastReply = idx === rest.length - 1; const lastNote = isLastSubthread && lastReply; return (
); })} ); }; export function ThreadRoute({ id }: { id?: string }) { const params = useParams(); const resolvedId = id ?? params.id; const link = parseNostrLink(resolvedId ?? "", NostrPrefix.Note); return ( ); } export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean }) { const thread = useContext(ThreadContext); const navigate = useNavigate(); const isSingleNote = thread.chains?.size === 1 && [thread.chains.values].every(v => v.length === 0); const { formatMessage } = useIntl(); function navigateThread(e: TaggedNostrEvent) { thread.setCurrent(e.id); //router.navigate(`/${NostrLink.fromEvent(e).encode()}`, { replace: true }) } const parent = useMemo(() => { if (thread.root) { const currentThread = EventExt.extractThread(thread.root); return ( currentThread?.replyTo?.value ?? currentThread?.root?.value ?? (currentThread?.root?.key === "a" && currentThread.root?.value) ); } }, [thread.root]); function renderRoot(note: TaggedNostrEvent) { const className = `thread-root${isSingleNote ? " thread-root-single" : ""}`; if (note) { return ( ); } else { return Loading thread root.. ({thread.data?.length} notes loaded); } } function renderChain(from: u256): ReactNode { if (!from || thread.chains.size === 0) { return; } const replies = thread.chains.get(from); if (replies && thread.current) { return ( NostrLink.fromEvent(a)), )} chains={thread.chains} onNavigate={navigateThread} /> ); } } function goBack() { if (parent) { thread.setCurrent(parent); } else if (props.onBack) { props.onBack(); } else { navigate(-1); } } const parentText = formatMessage({ defaultMessage: "Parent", id: "ADmfQT", description: "Link to parent note in thread", }); const debug = window.location.search.includes("debug=true"); return ( <> {debug && (

Chains

            {JSON.stringify(
              Object.fromEntries([...thread.chains.entries()].map(([k, v]) => [k, v.map(c => c.id)])),
              undefined,
              "  ",
            )}
          

Current

{JSON.stringify(thread.current)}

Root

{JSON.stringify(thread.root, undefined, "  ")}

Data

{JSON.stringify(thread.data, undefined, "  ")}

Reactions

{JSON.stringify(thread.reactions, undefined, "  ")}
)} {parent && (
)}
{thread.root && renderRoot(thread.root)} {thread.root && renderChain(chainKey(thread.root))}
); } function getReplies(from: u256, chains?: Map>): Array { if (!from || !chains) { return []; } const replies = chains.get(from); return replies ? replies : []; }