feat: render replies to a tags

This commit is contained in:
2023-04-25 11:04:20 +01:00
parent 5ec7c1a765
commit 7ca87fb4bd
5 changed files with 59 additions and 34 deletions

View File

@ -5,7 +5,7 @@ import { useNavigate, useLocation, Link, useParams } from "react-router-dom";
import { TaggedRawEvent, u256, EventKind, encodeTLV, NostrPrefix } from "@snort/nostr"; import { TaggedRawEvent, u256, EventKind, encodeTLV, NostrPrefix } from "@snort/nostr";
import { EventExt, Thread as ThreadInfo } from "System/EventExt"; 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 BackButton from "Element/BackButton";
import Note from "Element/Note"; import Note from "Element/Note";
import NoteGhost from "Element/NoteGhost"; import NoteGhost from "Element/NoteGhost";
@ -29,7 +29,6 @@ const Divider = ({ variant = "regular" }: DividerProps) => {
interface SubthreadProps { interface SubthreadProps {
isLastSubthread?: boolean; isLastSubthread?: boolean;
from: u256;
active: u256; active: u256;
notes: readonly TaggedRawEvent[]; notes: readonly TaggedRawEvent[];
related: readonly TaggedRawEvent[]; related: readonly TaggedRawEvent[];
@ -59,7 +58,6 @@ const Subthread = ({ active, notes, related, chains, onNavigate }: SubthreadProp
<TierTwo <TierTwo
active={active} active={active}
isLastSubthread={isLastSubthread} isLastSubthread={isLastSubthread}
from={a.id}
notes={replies} notes={replies}
related={related} related={related}
chains={chains} chains={chains}
@ -78,7 +76,7 @@ interface ThreadNoteProps extends Omit<SubthreadProps, "notes"> {
isLast: boolean; 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 { formatMessage } = useIntl();
const replies = getReplies(note.id, chains); const replies = getReplies(note.id, chains);
const activeInReplies = replies.map(r => r.id).includes(active); const activeInReplies = replies.map(r => r.id).includes(active);
@ -105,7 +103,6 @@ const ThreadNote = ({ active, note, isLast, isLastSubthread, from, related, chai
<TierThree <TierThree
active={active} active={active}
isLastSubthread={isLastSubthread} isLastSubthread={isLastSubthread}
from={from}
notes={replies} notes={replies}
related={related} related={related}
chains={chains} chains={chains}
@ -117,14 +114,13 @@ const ThreadNote = ({ active, note, isLast, isLastSubthread, from, related, chai
); );
}; };
const TierTwo = ({ active, isLastSubthread, from, notes, related, chains, onNavigate }: SubthreadProps) => { const TierTwo = ({ active, isLastSubthread, notes, related, chains, onNavigate }: SubthreadProps) => {
const [first, ...rest] = notes; const [first, ...rest] = notes;
return ( return (
<> <>
<ThreadNote <ThreadNote
active={active} active={active}
from={from}
onNavigate={onNavigate} onNavigate={onNavigate}
note={first} note={first}
chains={chains} chains={chains}
@ -138,7 +134,6 @@ const TierTwo = ({ active, isLastSubthread, from, notes, related, chains, onNavi
return ( return (
<ThreadNote <ThreadNote
active={active} active={active}
from={from}
onNavigate={onNavigate} onNavigate={onNavigate}
note={r} note={r}
chains={chains} chains={chains}
@ -152,7 +147,7 @@ const TierTwo = ({ active, isLastSubthread, from, notes, related, chains, onNavi
); );
}; };
const TierThree = ({ active, isLastSubthread, from, notes, related, chains, onNavigate }: SubthreadProps) => { const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate }: SubthreadProps) => {
const [first, ...rest] = notes; const [first, ...rest] = notes;
const replies = getReplies(first.id, chains); const replies = getReplies(first.id, chains);
const hasMultipleNotes = rest.length > 0 || replies.length > 0; const hasMultipleNotes = rest.length > 0 || replies.length > 0;
@ -178,7 +173,6 @@ const TierThree = ({ active, isLastSubthread, from, notes, related, chains, onNa
<TierThree <TierThree
active={active} active={active}
isLastSubthread={isLastSubthread} isLastSubthread={isLastSubthread}
from={from}
notes={replies} notes={replies}
related={related} related={related}
chains={chains} chains={chains}
@ -216,7 +210,7 @@ export default function Thread() {
const params = useParams(); const params = useParams();
const location = useLocation(); const location = useLocation();
const link = parseNostrLink(params.id ?? ""); const link = parseNostrLink(params.id ?? "", NostrPrefix.Note);
const thread = useThreadFeed(unwrap(link)); const thread = useThreadFeed(unwrap(link));
const [currentId, setCurrentId] = useState(link?.id); const [currentId, setCurrentId] = useState(link?.id);
@ -240,16 +234,20 @@ export default function Thread() {
?.filter(a => a.kind === EventKind.TextNote) ?.filter(a => a.kind === EventKind.TextNote)
.sort((a, b) => b.created_at - a.created_at) .sort((a, b) => b.created_at - a.created_at)
.forEach(v => { .forEach(v => {
const thread = EventExt.extractThread(v); const t = EventExt.extractThread(v);
const replyTo = thread?.replyTo?.Event ?? thread?.root?.Event; 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 (replyTo) {
if (!chains.has(replyTo)) { if (!chains.has(replyTo)) {
chains.set(replyTo, [v]); chains.set(replyTo, [v]);
} else { } else {
unwrap(chains.get(replyTo)).push(v); 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)) { if (isRoot(currentThread)) {
return currentNote; 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 // 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) {
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 => { const possibleRoots = thread.data?.filter(a => {
@ -295,7 +301,7 @@ export default function Thread() {
const parent = useMemo(() => { const parent = useMemo(() => {
if (root) { if (root) {
const currentThread = EventExt.extractThread(root); const currentThread = EventExt.extractThread(root);
return currentThread?.replyTo?.Event ?? currentThread?.root?.Event; return currentThread?.replyTo?.Event ?? currentThread?.root?.Event ?? currentThread?.root?.ATag;
} }
}, [root]); }, [root]);
@ -328,7 +334,6 @@ export default function Thread() {
return ( return (
<Subthread <Subthread
active={currentId} active={currentId}
from={from}
notes={replies} notes={replies}
related={getAllReactions( related={getAllReactions(
thread.data, thread.data,

View File

@ -8,6 +8,7 @@ import useLogin from "Hooks/useLogin";
export default function useThreadFeed(link: NostrLink) { export default function useThreadFeed(link: NostrLink) {
const [trackingEvents, setTrackingEvent] = useState<u256[]>([link.id]); const [trackingEvents, setTrackingEvent] = useState<u256[]>([link.id]);
const [trackingATags, setTrackingATags] = useState<string[]>([]);
const [allEvents, setAllEvents] = useState<u256[]>([link.id]); const [allEvents, setAllEvents] = useState<u256[]>([link.id]);
const pref = useLogin().preferences; const pref = useLogin().preferences;
@ -26,8 +27,19 @@ export default function useThreadFeed(link: NostrLink) {
) )
.tag("e", allEvents); .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; return sub;
}, [trackingEvents, allEvents, pref, link.id]); }, [trackingEvents, trackingATags, allEvents, pref, link.id]);
const store = useRequestBuilder<FlatNoteStore>(FlatNoteStore, sub); const store = useRequestBuilder<FlatNoteStore>(FlatNoteStore, sub);
@ -39,6 +51,9 @@ export default function useThreadFeed(link: NostrLink) {
const eTagsMissing = eTags.filter(a => !mainNotes.some(b => b.id === a)); const eTagsMissing = eTags.filter(a => !mainNotes.some(b => b.id === a));
setTrackingEvent(s => appendDedupe(s, eTagsMissing)); setTrackingEvent(s => appendDedupe(s, eTagsMissing));
setAllEvents(s => appendDedupe(s, eTags)); 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]); }, [store]);

View File

@ -72,7 +72,7 @@ export abstract class EventExt {
} }
static extractThread(ev: RawEvent) { 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) { if (!isThread) {
return undefined; return undefined;
} }
@ -82,7 +82,7 @@ export abstract class EventExt {
mentions: [], mentions: [],
pubKeys: [], pubKeys: [],
} as Thread; } 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); const marked = eTags.some(a => a.Marker !== undefined);
if (!marked) { if (!marked) {
ret.root = eTags[0]; ret.root = eTags[0];

View File

@ -61,7 +61,7 @@ export function bech32ToHex(str: string) {
const buff = bech32.fromWords(nKey.words); const buff = bech32.fromWords(nKey.words);
return secp.utils.bytesToHex(Uint8Array.from(buff)); return secp.utils.bytesToHex(Uint8Array.from(buff));
} catch { } 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; 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); const id = bech32ToHex(entity);
return { return {
type: NostrPrefix.PublicKey, type: NostrPrefix.PublicKey,
id: id, id: id,
encode: () => hexToBech32(NostrPrefix.PublicKey, id), encode: () => hexToBech32(NostrPrefix.PublicKey, id),
}; };
} else if (entity.startsWith(NostrPrefix.Note)) { } else if (isPrefix(NostrPrefix.Note)) {
const id = bech32ToHex(entity); const id = bech32ToHex(entity);
return { return {
type: NostrPrefix.Note, type: NostrPrefix.Note,
id: id, id: id,
encode: () => hexToBech32(NostrPrefix.Note, id), encode: () => hexToBech32(NostrPrefix.Note, id),
}; };
} else if ( } else if (isPrefix(NostrPrefix.Profile) || isPrefix(NostrPrefix.Event) || isPrefix(NostrPrefix.Address)) {
entity.startsWith(NostrPrefix.Profile) ||
entity.startsWith(NostrPrefix.Event) ||
entity.startsWith(NostrPrefix.Address)
) {
const decoded = decodeTLV(entity); const decoded = decodeTLV(entity);
const id = decoded.find(a => a.type === TLVEntryType.Special)?.value as string; 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 = () => { const encode = () => {
return entity; // return original return entity; // return original
}; };
if (entity.startsWith(NostrPrefix.Profile)) { if (isPrefix(NostrPrefix.Profile)) {
return { return {
type: NostrPrefix.Profile, type: NostrPrefix.Profile,
id, id,
@ -560,7 +560,7 @@ export function parseNostrLink(link: string): NostrLink | undefined {
author, author,
encode, encode,
}; };
} else if (entity.startsWith(NostrPrefix.Event)) { } else if (isPrefix(NostrPrefix.Event)) {
return { return {
type: NostrPrefix.Event, type: NostrPrefix.Event,
id, id,
@ -569,7 +569,7 @@ export function parseNostrLink(link: string): NostrLink | undefined {
author, author,
encode, encode,
}; };
} else if (entity.startsWith(NostrPrefix.Address)) { } else if (isPrefix(NostrPrefix.Address)) {
return { return {
type: NostrPrefix.Address, type: NostrPrefix.Address,
id, id,

View File

@ -10,6 +10,7 @@ export default class Tag {
Marker?: string; Marker?: string;
Hashtag?: string; Hashtag?: string;
DTag?: string; DTag?: string;
ATag?: string;
Index: number; Index: number;
Invalid: boolean; Invalid: boolean;
LNURL?: string; LNURL?: string;
@ -43,6 +44,10 @@ export default class Tag {
this.DTag = tag[1]; this.DTag = tag[1];
break; break;
} }
case "a": {
this.ATag = tag[1];
break;
}
case "t": { case "t": {
this.Hashtag = tag[1]; this.Hashtag = tag[1];
break; break;