diff --git a/packages/app/src/Element/Note.tsx b/packages/app/src/Element/Note.tsx index 10115754..ef824778 100644 --- a/packages/app/src/Element/Note.tsx +++ b/packages/app/src/Element/Note.tsx @@ -246,8 +246,8 @@ export default function Note(props: NoteProps) { } const maxMentions = 2; - const replyId = thread?.replyTo?.Event ?? thread?.root?.Event; - const replyRelayHints = thread?.replyTo?.Relay ?? thread.root?.Relay; + const replyId = thread?.replyTo?.value ?? thread?.root?.value; + const replyRelayHints = thread?.replyTo?.relay ?? thread.root?.relay; const mentions: { pk: string; name: string; link: ReactNode }[] = []; for (const pk of thread?.pubKeys ?? []) { const u = UserCache.getFromCache(pk); diff --git a/packages/app/src/Element/Thread.tsx b/packages/app/src/Element/Thread.tsx index f30f8966..7ab48b3d 100644 --- a/packages/app/src/Element/Thread.tsx +++ b/packages/app/src/Element/Thread.tsx @@ -239,9 +239,9 @@ export default function Thread() { .sort((a, b) => b.created_at - a.created_at) .forEach(v => { const t = EventExt.extractThread(v); - let replyTo = t?.replyTo?.Event ?? t?.root?.Event; - if (t?.root?.ATag) { - const parsed = t.root.ATag.split(":"); + let replyTo = t?.replyTo?.value ?? t?.root?.value; + if (t?.root?.key === "a" && t?.root?.value) { + const parsed = t.root.value.split(":"); replyTo = thread.data?.find( a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2] )?.id; @@ -274,14 +274,14 @@ export default function Thread() { // 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.ATag) { - const parsed = replyTo.ATag.split(":"); + if (replyTo.key === "a" && replyTo.value) { + const parsed = replyTo.value.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); + if (replyTo.value) { + return thread.data?.find(a => a.id === replyTo.value); } } @@ -305,7 +305,11 @@ export default function Thread() { const parent = useMemo(() => { if (root) { const currentThread = EventExt.extractThread(root); - return currentThread?.replyTo?.Event ?? currentThread?.root?.Event ?? currentThread?.root?.ATag; + return ( + currentThread?.replyTo?.value ?? + currentThread?.root?.value ?? + (currentThread?.root?.key === "a" && currentThread.root?.value) + ); } }, [root]); diff --git a/packages/system/src/EventExt.ts b/packages/system/src/EventExt.ts index c4fa03c8..25644c50 100644 --- a/packages/system/src/EventExt.ts +++ b/packages/system/src/EventExt.ts @@ -1,14 +1,21 @@ import * as secp from "@noble/curves/secp256k1"; import * as utils from "@noble/curves/abstract/utils"; -import { EventKind, HexKey, NostrEvent, Tag } from "."; +import { EventKind, HexKey, NostrEvent } from "."; import base64 from "@protobufjs/base64"; import { sha256, unixNow } from "./Utils"; +export interface Tag { + key: string + value?: string + relay?: string + marker?: string // NIP-10 +} + export interface Thread { - root?: Tag; - replyTo?: Tag; - mentions: Array; - pubKeys: Array; + root?: Tag + replyTo?: Tag + mentions: Array + pubKeys: Array } export abstract class EventExt { @@ -73,6 +80,24 @@ export abstract class EventExt { } as NostrEvent; } + static parseTag(tag: Array) { + if (tag.length < 1) { + throw new Error("Invalid tag, must have more than 2 items") + } + + const ret = { + key: tag[0], + value: tag[1] + } as Tag; + switch (ret.key) { + case "e": { + ret.relay = tag.length > 2 ? tag[2] : undefined; + ret.marker = tag.length > 3 ? tag[3] : undefined; + break; + } + } + return ret; + } static extractThread(ev: NostrEvent) { const isThread = ev.tags.some(a => (a[0] === "e" && a[3] !== "mention") || a[0] == "a"); if (!isThread) { @@ -84,27 +109,27 @@ export abstract class EventExt { mentions: [], pubKeys: [], } as Thread; - 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 eTags = ev.tags.filter(a => a[0] === "e" || a[0] === "a").map(a => EventExt.parseTag(a)); + const marked = eTags.some(a => a.marker); if (!marked) { ret.root = eTags[0]; - ret.root.Marker = shouldWriteMarkers ? "root" : undefined; + ret.root.marker = shouldWriteMarkers ? "root" : undefined; if (eTags.length > 1) { - ret.replyTo = eTags[1]; - ret.replyTo.Marker = shouldWriteMarkers ? "reply" : undefined; + ret.replyTo = eTags[eTags.length - 1]; + ret.replyTo.marker = shouldWriteMarkers ? "reply" : undefined; } if (eTags.length > 2) { - ret.mentions = eTags.slice(2); + ret.mentions = eTags.slice(1, -1); if (shouldWriteMarkers) { - ret.mentions.forEach(a => (a.Marker = "mention")); + ret.mentions.forEach(a => (a.marker = "mention")); } } } else { - const root = eTags.find(a => a.Marker === "root"); - const reply = eTags.find(a => a.Marker === "reply"); + const root = eTags.find(a => a.marker === "root"); + const reply = eTags.find(a => a.marker === "reply"); ret.root = root; ret.replyTo = reply; - ret.mentions = eTags.filter(a => a.Marker === "mention"); + ret.mentions = eTags.filter(a => a.marker === "mention"); } ret.pubKeys = Array.from(new Set(ev.tags.filter(a => a[0] === "p").map(a => a[1]))); return ret; diff --git a/packages/system/src/EventPublisher.ts b/packages/system/src/EventPublisher.ts index 5f388ddc..7a19b931 100644 --- a/packages/system/src/EventPublisher.ts +++ b/packages/system/src/EventPublisher.ts @@ -214,7 +214,7 @@ export class EventPublisher { const thread = EventExt.extractThread(replyTo); if (thread) { if (thread.root || thread.replyTo) { - eb.tag(["e", thread.root?.Event ?? thread.replyTo?.Event ?? "", "", "root"]); + eb.tag(["e", thread.root?.value ?? thread.replyTo?.value ?? "", "", "root"]); } eb.tag(["e", replyTo.id, replyTo.relays?.[0] ?? "", "reply"]); diff --git a/packages/system/src/Tag.ts b/packages/system/src/Tag.ts deleted file mode 100644 index b08128bd..00000000 --- a/packages/system/src/Tag.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { HexKey, u256 } from "./Nostr"; -import { unwrap } from "./Utils"; - -export default class Tag { - Original: string[]; - Key: string; - Event?: u256; - PubKey?: HexKey; - Relay?: string; - Marker?: string; - Hashtag?: string; - DTag?: string; - ATag?: string; - Index: number; - Invalid: boolean; - LNURL?: string; - - constructor(tag: string[], index: number) { - this.Original = tag; - this.Key = tag[0]; - this.Index = index; - this.Invalid = false; - - switch (this.Key) { - case "e": { - // ["e", , , ] - this.Event = tag[1]; - this.Relay = tag.length > 2 ? tag[2] : undefined; - this.Marker = tag.length > 3 ? tag[3] : undefined; - if (!this.Event) { - this.Invalid = true; - } - break; - } - case "p": { - // ["p", ] - this.PubKey = tag[1]; - if (!this.PubKey) { - this.Invalid = true; - } - break; - } - case "d": { - this.DTag = tag[1]; - break; - } - case "a": { - this.ATag = tag[1]; - break; - } - case "t": { - this.Hashtag = tag[1]; - break; - } - case "delegation": { - this.PubKey = tag[1]; - break; - } - case "zap": { - this.LNURL = tag[1]; - break; - } - } - } - - ToObject(): string[] | null { - switch (this.Key) { - case "e": { - let ret = ["e", this.Event, this.Relay, this.Marker]; - const trimEnd = ret.reverse().findIndex(a => a !== undefined); - ret = ret.reverse().slice(0, ret.length - trimEnd); - return ret; - } - case "p": { - return this.PubKey ? ["p", this.PubKey] : null; - } - case "t": { - return ["t", unwrap(this.Hashtag)]; - } - case "d": { - return ["d", unwrap(this.DTag)]; - } - default: { - return this.Original; - } - } - } -} diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts index 4f11456c..f6aab4a9 100644 --- a/packages/system/src/index.ts +++ b/packages/system/src/index.ts @@ -8,7 +8,6 @@ export * from "./NostrSystem"; export { default as EventKind } from "./EventKind"; export * from "./Nostr"; export * from "./Links"; -export { default as Tag } from "./Tag"; export * from "./Nips"; export * from "./RelayInfo"; export * from "./EventExt"; diff --git a/packages/system/tests/EventExt.test.ts b/packages/system/tests/EventExt.test.ts new file mode 100644 index 00000000..c6bec8d1 --- /dev/null +++ b/packages/system/tests/EventExt.test.ts @@ -0,0 +1,62 @@ +import { EventExt } from "../src/EventExt"; + +describe("NIP-10", () => { + it("should extract thread", () => { + const a = { + content: "This is the problem with Lightning....", + id: "868187063f...", + kind: 1, + created_at: 1, + pubkey: "test", + sig: "test", + "tags": [ + ["e", "cbf2375078..."], + ["e", "977ac5d3b6..."], + ["e", "8f99ca1363..."], + ] + } + + const b = { + "content": "This is a good point, but your ...", + "id": "434ad4a646...", + kind: 1, + created_at: 1, + pubkey: "test", + sig: "test", + "tags": [ + ["e", "cbf2375078..."], + ["e", "868187063f..."], + ["e", "6834ffc491..."], + ] + } + + const c = { + "content": "There is some middle ground ...", + "id": "6834ffc491...", + kind: 1, + created_at: 1, + pubkey: "test", + sig: "test", + "tags": [ + ["e", "cbf2375078...", "", "root"], + ["e", "868187063f...", "", "reply"], + ] + } + + expect(EventExt.extractThread(a)).toMatchObject({ + root: { key: "e", value: "cbf2375078...", marker: "root" }, + replyTo: { key: "e", value: "8f99ca1363...", marker: "reply" }, + mentions: [{ key: "e", value: "977ac5d3b6...", marker: "mention" }] + }) + expect(EventExt.extractThread(b)).toMatchObject({ + root: { key: "e", value: "cbf2375078...", marker: "root" }, + replyTo: { key: "e", value: "6834ffc491...", marker: "reply" }, + mentions: [{ key: "e", value: "868187063f...", marker: "mention" }] + }) + expect(EventExt.extractThread(c)).toMatchObject({ + root: { key: "e", value: "cbf2375078...", relay: "", marker: "root" }, + replyTo: { key: "e", value: "868187063f...", relay: "", marker: "reply" }, + mentions: [] + }) + }) +}) \ No newline at end of file