From e8519daa473b3e3d3dfaafa7cd28d21d29b05634 Mon Sep 17 00:00:00 2001 From: Kieran Date: Fri, 22 Sep 2023 15:09:13 +0100 Subject: [PATCH] Thread loading fixes --- packages/app/src/Element/Thread.tsx | 44 ++++++++---- packages/app/src/Feed/Reactions.ts | 25 +++++-- packages/app/src/Feed/ThreadFeed.ts | 37 +++++++++- packages/app/src/Hooks/useThreadContext.tsx | 77 ++++++++------------- packages/app/src/index.css | 2 +- packages/system/src/gossip-model.ts | 4 +- packages/system/src/request-builder.ts | 36 +++------- 7 files changed, 126 insertions(+), 99 deletions(-) diff --git a/packages/app/src/Element/Thread.tsx b/packages/app/src/Element/Thread.tsx index 628ddcf2..cbcc1b07 100644 --- a/packages/app/src/Element/Thread.tsx +++ b/packages/app/src/Element/Thread.tsx @@ -2,14 +2,14 @@ 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 { TaggedNostrEvent, u256, NostrPrefix, EventExt, parseNostrLink } from "@snort/system"; -import { getReactions, getAllReactions, unwrap } from "SnortUtils"; +import { getReactions, getAllReactions } from "SnortUtils"; import BackButton from "Element/BackButton"; import Note from "Element/Note"; import NoteGhost from "Element/NoteGhost"; import Collapsed from "Element/Collapsed"; -import { ThreadContext, ThreadContextWrapper } from "Hooks/useThreadContext"; +import { ThreadContext, ThreadContextWrapper, chainKey } from "Hooks/useThreadContext"; import messages from "./messages"; @@ -154,8 +154,9 @@ const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate return ( <>
+ className={`subthread-container ${hasMultipleNotes ? "subthread-multi" : ""} ${ + isLast ? "subthread-last" : "subthread-mid" + }`}> + className={`subthread-container ${lastReply ? "" : "subthread-multi"} ${ + lastReply ? "subthread-last" : "subthread-mid" + }`}> void }) { description: "Navigate back button on threads view", }); - const rootChainId = (ev: TaggedNostrEvent) => { - const link = NostrLink.fromEvent(ev); - return unwrap(link.toEventTag())[1]; - } - + 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, "  ")}
+
+ )}
{thread.root && renderRoot(thread.root)} - {thread.root && renderChain(rootChainId(thread.root))} + {thread.root && renderChain(chainKey(thread.root))}
); diff --git a/packages/app/src/Feed/Reactions.ts b/packages/app/src/Feed/Reactions.ts index 664268c3..db6d0a39 100644 --- a/packages/app/src/Feed/Reactions.ts +++ b/packages/app/src/Feed/Reactions.ts @@ -10,13 +10,24 @@ export function useReactions(subId: string, ids: Array, others?: (rb: const rb = new RequestBuilder(subId); if (ids.length > 0) { - rb - .withFilter() - .kinds( - pref.enableReactions - ? [EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt] - : [EventKind.ZapReceipt, EventKind.Repost], - ).replyToLink(ids); + const grouped = ids.reduce( + (acc, v) => { + acc[v.type] ??= []; + acc[v.type].push(v); + return acc; + }, + {} as Record>, + ); + + for (const [, v] of Object.entries(grouped)) { + rb.withFilter() + .kinds( + pref.enableReactions + ? [EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt] + : [EventKind.ZapReceipt, EventKind.Repost], + ) + .replyToLink(v); + } } others?.(rb); return rb.numFilters > 0 ? rb : null; diff --git a/packages/app/src/Feed/ThreadFeed.ts b/packages/app/src/Feed/ThreadFeed.ts index 4ebd1e10..6eef9946 100644 --- a/packages/app/src/Feed/ThreadFeed.ts +++ b/packages/app/src/Feed/ThreadFeed.ts @@ -1,10 +1,11 @@ import { useEffect, useMemo, useState } from "react"; -import { EventKind, NostrLink, RequestBuilder, NoteCollection } from "@snort/system"; +import { EventKind, NostrLink, RequestBuilder, NoteCollection, EventExt } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; import { useReactions } from "./Reactions"; export default function useThreadFeed(link: NostrLink) { + const [root, setRoot] = useState(); const [allEvents, setAllEvents] = useState>([]); const sub = useMemo(() => { @@ -13,7 +14,21 @@ export default function useThreadFeed(link: NostrLink) { leaveOpen: true, }); sub.withFilter().link(link); - sub.withFilter().kinds([EventKind.TextNote]).replyToLink([link, ...allEvents]); + if (root) { + sub.withFilter().link(root); + } + const grouped = [link, ...allEvents].reduce( + (acc, v) => { + acc[v.type] ??= []; + acc[v.type].push(v); + return acc; + }, + {} as Record>, + ); + + for (const [, v] of Object.entries(grouped)) { + sub.withFilter().kinds([EventKind.TextNote]).replyToLink(v); + } return sub; }, [allEvents.length]); @@ -28,6 +43,24 @@ export default function useThreadFeed(link: NostrLink) { ]) .flat(); setAllEvents(links); + + const current = store.data.find(a => link.matchesEvent(a)); + if (current) { + const t = EventExt.extractThread(current); + if (t) { + const rootOrReplyAsRoot = t?.root ?? t?.replyTo; + if (rootOrReplyAsRoot) { + setRoot( + NostrLink.fromTag([ + rootOrReplyAsRoot.key, + rootOrReplyAsRoot.value ?? "", + rootOrReplyAsRoot.relay ?? "", + ...(rootOrReplyAsRoot.marker ?? []), + ]), + ); + } + } + } } }, [store.data?.length]); diff --git a/packages/app/src/Hooks/useThreadContext.tsx b/packages/app/src/Hooks/useThreadContext.tsx index ee5ee31d..afc24229 100644 --- a/packages/app/src/Hooks/useThreadContext.tsx +++ b/packages/app/src/Hooks/useThreadContext.tsx @@ -1,7 +1,7 @@ +/* eslint-disable no-debugger */ import { unwrap } from "@snort/shared"; -import { EventExt, NostrLink, NostrPrefix, TaggedNostrEvent, u256, Thread as ThreadInfo } from "@snort/system"; +import { EventExt, NostrLink, TaggedNostrEvent, u256 } from "@snort/system"; import useThreadFeed from "Feed/ThreadFeed"; -import { findTag } from "SnortUtils"; import { ReactNode, createContext, useMemo, useState } from "react"; import { useLocation } from "react-router-dom"; @@ -10,21 +10,31 @@ export interface ThreadContext { root?: TaggedNostrEvent; chains: Map>; data: Array; + reactions: Array; setCurrent: (i: string) => void; } export const ThreadContext = createContext({} as ThreadContext); -export function threadChainKey(ev: TaggedNostrEvent) { +/** + * Get the chain key as a reply event + */ +export function replyChainKey(ev: TaggedNostrEvent) { const t = EventExt.extractThread(ev); - if (t) { - return unwrap(t.replyTo?.value ?? t.root?.value); - } + return t?.replyTo?.value ?? t?.root?.value; +} + +/** + * Get the chain key of this event + */ +export function chainKey(ev: TaggedNostrEvent) { + const link = NostrLink.fromEvent(ev); + return unwrap(link.toEventTag())[1]; } export function ThreadContextWrapper({ link, children }: { link: NostrLink; children?: ReactNode }) { const location = useLocation(); - const [currentId, setCurrentId] = useState(link.id); + const [currentId, setCurrentId] = useState(unwrap(link.toEventTag())[1]); const feed = useThreadFeed(link); const chains = useMemo(() => { @@ -33,7 +43,7 @@ export function ThreadContextWrapper({ link, children }: { link: NostrLink; chil feed.thread ?.sort((a, b) => b.created_at - a.created_at) .forEach(v => { - const replyTo = threadChainKey(v); + const replyTo = replyChainKey(v); if (replyTo) { if (!chains.has(replyTo)) { chains.set(replyTo, [v]); @@ -49,58 +59,27 @@ export function ThreadContextWrapper({ link, children }: { link: NostrLink; chil // Root is the parent of the current note or the current note if its a root note or the root of the thread const root = useMemo(() => { const currentNote = - feed.thread?.find( - ne => - ne.id === currentId || - (link.type === NostrPrefix.Address && findTag(ne, "d") === currentId && ne.pubkey === link.author), - ) ?? (location.state && "sig" in location.state ? (location.state as TaggedNostrEvent) : undefined); + feed.thread?.find(a => chainKey(a) === currentId) ?? + (location.state && "sig" in location.state ? (location.state as TaggedNostrEvent) : undefined); if (currentNote) { - const currentThread = EventExt.extractThread(currentNote); - const isRoot = (ne?: ThreadInfo) => ne === undefined; - - if (isRoot(currentThread)) { + const key = replyChainKey(currentNote); + if (key) { + return feed.thread?.find(a => chainKey(a) === key); + } else { return currentNote; } - const replyTo = currentThread?.replyTo ?? currentThread?.root; - - // sometimes the root event ID is missing, and we can only take the happy path if the root event ID exists - if (replyTo) { - if (replyTo.key === "a" && replyTo.value) { - const parsed = replyTo.value.split(":"); - return feed.thread?.find( - a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2], - ); - } - if (replyTo.value) { - return feed.thread?.find(a => a.id === replyTo.value); - } - } - - const possibleRoots = feed.thread?.filter(a => { - const thread = EventExt.extractThread(a); - return isRoot(thread); - }); - if (possibleRoots) { - // worst case we need to check every possible root to see which one contains the current note as a child - for (const ne of possibleRoots) { - const children = chains.get(ne.id) ?? []; - - if (children.find(ne => ne.id === currentId)) { - return ne; - } - } - } } }, [feed.thread.length, currentId, location]); - const ctxValue = useMemo(() => { + const ctxValue = useMemo(() => { return { current: currentId, root, chains, - data: feed.reactions, + reactions: feed.reactions, + data: feed.thread, setCurrent: v => setCurrentId(v), - } as ThreadContext; + }; }, [root, chains]); return {children}; diff --git a/packages/app/src/index.css b/packages/app/src/index.css index ff786ae9..c131f265 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -539,7 +539,7 @@ div.form-col { height: 100vh; } -small.xs { +.xs { font-size: small; } diff --git a/packages/system/src/gossip-model.ts b/packages/system/src/gossip-model.ts index 2f2d9be8..caa05792 100644 --- a/packages/system/src/gossip-model.ts +++ b/packages/system/src/gossip-model.ts @@ -84,7 +84,7 @@ export function splitByWriteRelays(cache: RelayCache, filter: ReqFilter): Array< }, }); } - debug("GOSSIP")("Picked %o", picked); + debug("GOSSIP")("Picked %O => %O", filter, picked); return picked; } @@ -119,7 +119,7 @@ export function splitFlatByWriteRelays(cache: RelayCache, input: Array) { + tag(key: "e" | "p" | "d" | "t" | "r" | "a" | "g" | string, value?: Array) { if (!value) return this; - this.#filter[`#${key}`] = appendDedupe(this.#filter[`#${key}`], value); + this.#filter[`#${key}`] = appendDedupe(this.#filter[`#${key}`] as Array, value); return this; } @@ -239,11 +239,10 @@ export class RequestFilterBuilder { this.tag("d", [link.id]) .kinds([unwrap(link.kind)]) .authors([unwrap(link.author)]); - link.relays?.forEach(v => this.relay(v)); } else { this.ids([link.id]); - link.relays?.forEach(v => this.relay(v)); } + link.relays?.forEach(v => this.relay(v)); return this; } @@ -251,27 +250,14 @@ export class RequestFilterBuilder { * Get replies to link with e/a tags */ replyToLink(links: Array) { - const grouped = links.reduce((acc, v) => { - acc[v.type] ??= []; - if (v.type === NostrPrefix.Address) { - acc[v.type].push(`${v.kind}:${v.author}:${v.id}`); - } else if (v.type === NostrPrefix.PublicKey || v.type === NostrPrefix.Profile) { - acc[v.type].push(v.id); - } else { - acc[v.type].push(v.id); - } - return acc; - }, {} as Record>); + const types = dedupe(links.map(a => a.type)); + if (types.length > 1) throw new Error("Cannot add multiple links of different kinds"); - for(const [k,v] of Object.entries(grouped)) { - if (k === NostrPrefix.Address) { - this.tag("a", v); - } else if (k === NostrPrefix.PublicKey || k === NostrPrefix.Profile) { - this.tag("p", v); - } else { - this.tag("e", v); - } - } + const tags = links.map(a => unwrap(a.toEventTag())); + this.tag( + tags[0][0], + tags.map(v => v[1]), + ); return this; }