diff --git a/packages/app/src/Element/Thread.tsx b/packages/app/src/Element/Thread.tsx index 1d6683d1..638288d0 100644 --- a/packages/app/src/Element/Thread.tsx +++ b/packages/app/src/Element/Thread.tsx @@ -5,7 +5,7 @@ import { useNavigate, useLocation, Link, useParams } from "react-router-dom"; import { TaggedRawEvent, u256, EventKind, encodeTLV, NostrPrefix } from "@snort/nostr"; import { EventExt, Thread as ThreadInfo } from "System/EventExt"; -import { eventLink, unwrap, getReactions, parseNostrLink, getAllReactions } from "Util"; +import { eventLink, unwrap, getReactions, parseNostrLink, getAllReactions, findTag } from "Util"; import BackButton from "Element/BackButton"; import Note from "Element/Note"; import NoteGhost from "Element/NoteGhost"; @@ -29,7 +29,6 @@ const Divider = ({ variant = "regular" }: DividerProps) => { interface SubthreadProps { isLastSubthread?: boolean; - from: u256; active: u256; notes: readonly TaggedRawEvent[]; related: readonly TaggedRawEvent[]; @@ -59,7 +58,6 @@ const Subthread = ({ active, notes, related, chains, onNavigate }: SubthreadProp { isLast: boolean; } -const ThreadNote = ({ active, note, isLast, isLastSubthread, from, related, chains, onNavigate }: ThreadNoteProps) => { +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); @@ -105,7 +103,6 @@ const ThreadNote = ({ active, note, isLast, isLastSubthread, from, related, chai { +const TierTwo = ({ active, isLastSubthread, notes, related, chains, onNavigate }: SubthreadProps) => { const [first, ...rest] = notes; 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; @@ -178,7 +173,6 @@ const TierThree = ({ active, isLastSubthread, from, notes, related, chains, onNa a.kind === EventKind.TextNote) .sort((a, b) => b.created_at - a.created_at) .forEach(v => { - const thread = EventExt.extractThread(v); - const replyTo = thread?.replyTo?.Event ?? thread?.root?.Event; + const t = EventExt.extractThread(v); + let replyTo = t?.replyTo?.Event ?? t?.root?.Event; + if (t?.root?.ATag) { + const parsed = t.root.ATag.split(":"); + replyTo = thread.data?.find( + a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2] + )?.id; + } if (replyTo) { if (!chains.has(replyTo)) { chains.set(replyTo, [v]); } else { unwrap(chains.get(replyTo)).push(v); } - } else if (v.tags.length > 0) { - //console.log("Not replying to anything: ", v); } }); } @@ -268,11 +266,19 @@ export default function Thread() { if (isRoot(currentThread)) { return currentNote; } - const replyTo = currentThread?.replyTo?.Event ?? currentThread?.root?.Event; + 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) { - return thread.data?.find(a => a.id === replyTo); + if (replyTo.ATag) { + const parsed = replyTo.ATag.split(":"); + return thread.data?.find( + a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2] + ); + } + if (replyTo.Event) { + return thread.data?.find(a => a.id === replyTo.Event); + } } const possibleRoots = thread.data?.filter(a => { @@ -295,7 +301,7 @@ export default function Thread() { const parent = useMemo(() => { if (root) { const currentThread = EventExt.extractThread(root); - return currentThread?.replyTo?.Event ?? currentThread?.root?.Event; + return currentThread?.replyTo?.Event ?? currentThread?.root?.Event ?? currentThread?.root?.ATag; } }, [root]); @@ -328,7 +334,6 @@ export default function Thread() { return ( ([link.id]); + const [trackingATags, setTrackingATags] = useState([]); const [allEvents, setAllEvents] = useState([link.id]); const pref = useLogin().preferences; @@ -26,8 +27,19 @@ export default function useThreadFeed(link: NostrLink) { ) .tag("e", allEvents); + if (trackingATags.length > 0) { + const parsed = trackingATags.map(a => a.split(":")); + sub + .withFilter() + .kinds(parsed.map(a => Number(a[0]))) + .authors(parsed.map(a => a[1])) + .tag( + "d", + parsed.map(a => a[2]) + ); + } return sub; - }, [trackingEvents, allEvents, pref, link.id]); + }, [trackingEvents, trackingATags, allEvents, pref, link.id]); const store = useRequestBuilder(FlatNoteStore, sub); @@ -39,6 +51,9 @@ export default function useThreadFeed(link: NostrLink) { const eTagsMissing = eTags.filter(a => !mainNotes.some(b => b.id === a)); setTrackingEvent(s => appendDedupe(s, eTagsMissing)); setAllEvents(s => appendDedupe(s, eTags)); + + const aTags = mainNotes.map(a => a.tags.filter(b => b[0] === "a").map(b => b[1])).flat(); + setTrackingATags(s => appendDedupe(s, aTags)); } }, [store]); diff --git a/packages/app/src/System/EventExt.ts b/packages/app/src/System/EventExt.ts index 50c00589..26929468 100644 --- a/packages/app/src/System/EventExt.ts +++ b/packages/app/src/System/EventExt.ts @@ -72,7 +72,7 @@ export abstract class EventExt { } static extractThread(ev: RawEvent) { - const isThread = ev.tags.some(a => a[0] === "e" && a[3] !== "mention"); + const isThread = ev.tags.some(a => (a[0] === "e" && a[3] !== "mention") || a[0] == "a"); if (!isThread) { return undefined; } @@ -82,7 +82,7 @@ export abstract class EventExt { mentions: [], pubKeys: [], } as Thread; - const eTags = ev.tags.filter(a => a[0] === "e").map((v, i) => new Tag(v, i)); + const eTags = ev.tags.filter(a => a[0] === "e" || a[0] === "a").map((v, i) => new Tag(v, i)); const marked = eTags.some(a => a.Marker !== undefined); if (!marked) { ret.root = eTags[0]; diff --git a/packages/app/src/Util.ts b/packages/app/src/Util.ts index 588579b1..70326d5a 100644 --- a/packages/app/src/Util.ts +++ b/packages/app/src/Util.ts @@ -61,7 +61,7 @@ export function bech32ToHex(str: string) { const buff = bech32.fromWords(nKey.words); return secp.utils.bytesToHex(Uint8Array.from(buff)); } catch { - return ""; + return str; } } @@ -519,28 +519,28 @@ export function validateNostrLink(link: string): boolean { } } -export function parseNostrLink(link: string): NostrLink | undefined { +export function parseNostrLink(link: string, prefixHint?: NostrPrefix): NostrLink | undefined { const entity = link.startsWith("web+nostr:") || link.startsWith("nostr:") ? link.split(":")[1] : link; - if (entity.startsWith(NostrPrefix.PublicKey)) { + const isPrefix = (prefix: NostrPrefix) => { + return entity.startsWith(prefix) || prefix === prefixHint; + }; + + if (isPrefix(NostrPrefix.PublicKey)) { const id = bech32ToHex(entity); return { type: NostrPrefix.PublicKey, id: id, encode: () => hexToBech32(NostrPrefix.PublicKey, id), }; - } else if (entity.startsWith(NostrPrefix.Note)) { + } else if (isPrefix(NostrPrefix.Note)) { const id = bech32ToHex(entity); return { type: NostrPrefix.Note, id: id, encode: () => hexToBech32(NostrPrefix.Note, id), }; - } else if ( - entity.startsWith(NostrPrefix.Profile) || - entity.startsWith(NostrPrefix.Event) || - entity.startsWith(NostrPrefix.Address) - ) { + } else if (isPrefix(NostrPrefix.Profile) || isPrefix(NostrPrefix.Event) || isPrefix(NostrPrefix.Address)) { const decoded = decodeTLV(entity); const id = decoded.find(a => a.type === TLVEntryType.Special)?.value as string; @@ -551,7 +551,7 @@ export function parseNostrLink(link: string): NostrLink | undefined { const encode = () => { return entity; // return original }; - if (entity.startsWith(NostrPrefix.Profile)) { + if (isPrefix(NostrPrefix.Profile)) { return { type: NostrPrefix.Profile, id, @@ -560,7 +560,7 @@ export function parseNostrLink(link: string): NostrLink | undefined { author, encode, }; - } else if (entity.startsWith(NostrPrefix.Event)) { + } else if (isPrefix(NostrPrefix.Event)) { return { type: NostrPrefix.Event, id, @@ -569,7 +569,7 @@ export function parseNostrLink(link: string): NostrLink | undefined { author, encode, }; - } else if (entity.startsWith(NostrPrefix.Address)) { + } else if (isPrefix(NostrPrefix.Address)) { return { type: NostrPrefix.Address, id, diff --git a/packages/nostr/src/legacy/Tag.ts b/packages/nostr/src/legacy/Tag.ts index 9b197832..b3ecfb98 100644 --- a/packages/nostr/src/legacy/Tag.ts +++ b/packages/nostr/src/legacy/Tag.ts @@ -10,6 +10,7 @@ export default class Tag { Marker?: string; Hashtag?: string; DTag?: string; + ATag?: string; Index: number; Invalid: boolean; LNURL?: string; @@ -43,6 +44,10 @@ export default class Tag { this.DTag = tag[1]; break; } + case "a": { + this.ATag = tag[1]; + break; + } case "t": { this.Hashtag = tag[1]; break;