import "./Thread.css"; import { useMemo, useState, useEffect, ReactNode } from "react"; import { useSelector } from "react-redux"; import { useNavigate, useLocation, Link } from "react-router-dom"; import { TaggedRawEvent, u256, HexKey } from "Nostr"; import { default as NEvent } from "Nostr/Event"; import EventKind from "Nostr/EventKind"; import { eventLink, hexToBech32, bech32ToHex } from "Util"; import BackButton from "Element/BackButton"; import Note from "Element/Note"; import NoteGhost from "Element/NoteGhost"; import Collapsed from "Element/Collapsed"; import type { RootState } from "State/Store"; function getParent(ev: HexKey, chains: Map): HexKey | undefined { for (let [k, vs] of chains.entries()) { const fs = vs.map(a => a.Id) if (fs.includes(ev)) { return k } } } interface DividerProps { variant?: "regular" | "small" } const Divider = ({ variant = "regular" }: DividerProps) => { const className = variant === "small" ? "divider divider-small" : "divider" return (
) } interface SubthreadProps { isLastSubthread?: boolean from: u256 active: u256 path: u256[] notes: NEvent[] related: TaggedRawEvent[] chains: Map onNavigate: (e: u256) => void } const Subthread = ({ active, path, from, notes, related, chains, onNavigate }: SubthreadProps) => { const renderSubthread = (a: NEvent, 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: NEvent isLast: boolean } const ThreadNote = ({ active, note, isLast, path, isLastSubthread, from, related, chains, onNavigate }: ThreadNoteProps) => { const replies = getReplies(note.Id, chains) const activeInReplies = replies.map(r => r.Id).includes(active) const [collapsed, setCollapsed] = useState(!activeInReplies) const hasMultipleNotes = replies.length > 0 const isLastVisibleNote = isLastSubthread && isLast && !hasMultipleNotes const className = `subthread-container ${isLast && collapsed ? 'subthread-last' : 'subthread-multi subthread-mid'}` return ( <>
{replies.length > 0 && ( activeInReplies ? ( ) : ( ) )} ) } const TierTwo = ({ active, isLastSubthread, path, from, notes, related, chains, onNavigate }: SubthreadProps) => { const [first, ...rest] = notes return ( <> {rest.map((r: NEvent, idx: number) => { const lastReply = idx === rest.length - 1 return ( ) }) } ) } const TierThree = ({ active, path, isLastSubthread, from, notes, related, chains, onNavigate }: SubthreadProps) => { const [first, ...rest] = notes const replies = getReplies(first.Id, chains) const activeInReplies = notes.map(r => r.Id).includes(active) || replies.map(r => r.Id).includes(active) const hasMultipleNotes = rest.length > 0 || replies.length > 0 const isLast = replies.length === 0 && rest.length === 0 return ( <>
{path.length <= 1 || !activeInReplies ? ( replies.length > 0 && (
) ) : ( replies.length > 0 && ( ) )} {rest.map((r: NEvent, idx: number) => { const lastReply = idx === rest.length - 1 const lastNote = isLastSubthread && lastReply return (
) }) } ) } export interface ThreadProps { this?: u256, notes?: TaggedRawEvent[] } export default function Thread(props: ThreadProps) { const notes = props.notes ?? []; const parsedNotes = notes.map(a => new NEvent(a)); // root note has no thread info const root = useMemo(() => parsedNotes.find(a => a.Thread === null), [notes]); const [path, setPath] = useState([]) const currentId = path.length > 0 && path[path.length - 1] const currentRoot = useMemo(() => parsedNotes.find(a => a.Id === currentId), [notes, currentId]); const [navigated, setNavigated] = useState(false) const navigate = useNavigate() const isSingleNote = parsedNotes.filter(a => a.Kind === EventKind.TextNote).length === 1 const location = useLocation() const urlNoteId = location?.pathname.slice(3) const urlNoteHex = urlNoteId && bech32ToHex(urlNoteId) const rootNoteId = root && hexToBech32('note', root.Id) const chains = useMemo(() => { let chains = new Map(); parsedNotes?.filter(a => a.Kind === EventKind.TextNote).sort((a, b) => b.CreatedAt - a.CreatedAt).forEach((v) => { let replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event; if (replyTo) { if (!chains.has(replyTo)) { chains.set(replyTo, [v]); } else { chains.get(replyTo)!.push(v); } } else if (v.Tags.length > 0) { console.log("Not replying to anything: ", v); } }); return chains; }, [notes]); useEffect(() => { if (!root) { return } if (navigated) { return } if (root.Id === urlNoteHex) { setPath([root.Id]) setNavigated(true) return } let subthreadPath = [] let parent = getParent(urlNoteHex, chains) while (parent) { subthreadPath.unshift(parent) parent = getParent(parent, chains) } setPath(subthreadPath) setNavigated(true) }, [root, navigated, urlNoteHex, chains]) const brokenChains = useMemo(() => { return Array.from(chains?.keys()).filter(a => !parsedNotes?.some(b => b.Id === a)); }, [chains]); function renderRoot(note: NEvent) { const className = `thread-root ${isSingleNote ? 'thread-root-single' : ''}` if (note) { return } else { return ( Loading thread root.. ({notes?.length} notes loaded) ) } } function onNavigate(to: u256) { setPath([...path, to]) } function renderChain(from: u256): ReactNode { if (!from || !chains) { return } let replies = chains.get(from); if (replies) { return } } function goBack() { if (path.length > 1) { const newPath = path.slice(0, path.length - 1) setPath(newPath) } else { navigate("/") } } return (
1 ? "Parent" : "Back"} />
{currentRoot && renderRoot(currentRoot)} {currentRoot && renderChain(currentRoot.Id)} {currentRoot === root && ( <> {brokenChains.length > 0 &&

Other replies

} {brokenChains.map(a => { return (
Missing event {a.substring(0, 8)} {renderChain(a)}
) })} )}
); } function getReplies(from: u256, chains?: Map): NEvent[] { if (!from || !chains) { return [] } let replies = chains.get(from); return replies ? replies : [] }