diff --git a/nap.yaml b/nap.yaml
index b0bd7f15..fb1996d1 100644
--- a/nap.yaml
+++ b/nap.yaml
@@ -8,4 +8,4 @@ repository: "https://github.com/v0l/snort"
license: "MIT"
tags:
- "social"
- - "twitter"
\ No newline at end of file
+ - "twitter"
diff --git a/packages/app/src/Components/Event/Create/NoteCreator.tsx b/packages/app/src/Components/Event/Create/NoteCreator.tsx
index 248299cd..cfe76b5a 100644
--- a/packages/app/src/Components/Event/Create/NoteCreator.tsx
+++ b/packages/app/src/Components/Event/Create/NoteCreator.tsx
@@ -139,7 +139,6 @@ export function NoteCreator() {
extraTags ??= [];
extraTags.push(["content-warning", note.sensitive]);
}
- const kind = note.pollOptions ? EventKind.Polls : EventKind.TextNote;
if (note.pollOptions) {
extraTags ??= [];
extraTags.push(...note.pollOptions.map((a, i) => ["poll_option", i.toString(), a]));
@@ -179,7 +178,9 @@ export function NoteCreator() {
const hk = (eb: EventBuilder) => {
extraTags?.forEach(t => eb.tag(t));
note.extraTags?.forEach(t => eb.tag(t));
- eb.kind(kind);
+ if (note.pollOptions) {
+ eb.kind(EventKind.Polls);
+ }
return eb;
};
const ev = note.replyTo
diff --git a/packages/app/src/Components/Event/EventComponent.tsx b/packages/app/src/Components/Event/EventComponent.tsx
index 2e68c66b..cd46ac93 100644
--- a/packages/app/src/Components/Event/EventComponent.tsx
+++ b/packages/app/src/Components/Event/EventComponent.tsx
@@ -1,6 +1,6 @@
import "./EventComponent.css";
-import { EventKind, NostrEvent, TaggedNostrEvent } from "@snort/system";
+import { EventKind, NostrEvent, parseIMeta, TaggedNostrEvent } from "@snort/system";
import { memo, ReactNode } from "react";
import PubkeyList from "@/Components/Embed/PubkeyList";
@@ -75,6 +75,21 @@ export default memo(function EventComponent(props: NoteProps) {
case 9041: // Assuming 9041 is a valid EventKind
content = ;
break;
+ case EventKind.Photo:
+ case EventKind.Video:
+ case EventKind.ShortVideo: {
+ // append media to note as if kind1 post
+ const media = parseIMeta(ev.tags);
+ // Sometimes we cann call this twice so check the URL's are not already
+ // in the content
+ const urls = Object.entries(media ?? {}).map(([k]) => k);
+ if (!urls.every(u => ev.content.includes(u))) {
+ const newContent = ev.content + " " + urls.join("\n");
+ props.data.content = newContent;
+ }
+ content = ;
+ break;
+ }
case EventKind.LongFormTextNote:
content = (
({ maxSize: 300 });
export function Note(props: NoteProps) {
diff --git a/packages/app/src/Feed/ThreadFeed.ts b/packages/app/src/Feed/ThreadFeed.ts
index 1c52d9c7..ced5002e 100644
--- a/packages/app/src/Feed/ThreadFeed.ts
+++ b/packages/app/src/Feed/ThreadFeed.ts
@@ -1,4 +1,4 @@
-import { EventKind, Nip10, NostrLink, RequestBuilder } from "@snort/system";
+import { EventKind, Nip10, Nip22, NostrLink, RequestBuilder } from "@snort/system";
import { SnortContext, useRequestBuilder } from "@snort/system-react";
import { useContext, useEffect, useMemo, useState } from "react";
@@ -34,7 +34,7 @@ export default function useThreadFeed(link: NostrLink) {
for (const v of Object.values(grouped)) {
sub
.withFilter()
- .kinds([EventKind.TextNote])
+ .kinds([EventKind.TextNote, EventKind.Comment])
.replyToLink(v)
.relay(rootRelays ?? []);
}
@@ -48,7 +48,9 @@ export default function useThreadFeed(link: NostrLink) {
const links = store
.map(a => [
NostrLink.fromEvent(a),
- ...a.tags.filter(a => a[0] === "e" || a[0] === "a").map(v => NostrLink.fromTag(v)),
+ ...a.tags
+ .filter(a => a[0] === "e" || a[0] === "a" || a[0] === "E" || a[0] === "A")
+ .map(v => NostrLink.fromTag(v)),
])
.flat();
setAllEvents(links);
@@ -56,14 +58,19 @@ export default function useThreadFeed(link: NostrLink) {
// load the thread structure from the current note
const current = store.find(a => link.matchesEvent(a));
if (current) {
- const t = Nip10.parseThread(current);
- if (t) {
- const rootOrReplyAsRoot = t?.root ?? t?.replyTo;
- if (rootOrReplyAsRoot) {
- setRoot(rootOrReplyAsRoot);
+ if (current.kind === EventKind.TextNote) {
+ const t = Nip10.parseThread(current);
+ if (t) {
+ const rootOrReplyAsRoot = t?.root ?? t?.replyTo;
+ if (rootOrReplyAsRoot) {
+ setRoot(rootOrReplyAsRoot);
+ }
+ } else {
+ setRoot(link);
}
} else {
- setRoot(link);
+ const root = Nip22.rootScopeOf(current);
+ setRoot(NostrLink.fromTag(root));
}
}
}
diff --git a/packages/app/src/Utils/Thread/ChainKey.tsx b/packages/app/src/Utils/Thread/ChainKey.tsx
index 31d2998d..a0b53070 100644
--- a/packages/app/src/Utils/Thread/ChainKey.tsx
+++ b/packages/app/src/Utils/Thread/ChainKey.tsx
@@ -1,4 +1,4 @@
-import { Nip10, NostrLink, TaggedNostrEvent } from "@snort/system";
+import { EventKind, Nip10, NostrLink, TaggedNostrEvent } from "@snort/system";
/**
* Get the chain key as a reply event
@@ -6,9 +6,14 @@ import { Nip10, NostrLink, TaggedNostrEvent } from "@snort/system";
* ie. Get the key for which this event is replying to
*/
export function replyChainKey(ev: TaggedNostrEvent) {
- const t = Nip10.parseThread(ev);
- const tag = t?.replyTo ?? t?.root;
- return tag?.tagKey;
+ if (ev.kind !== EventKind.Comment) {
+ const t = Nip10.parseThread(ev);
+ const tag = t?.replyTo ?? t?.root;
+ return tag?.tagKey;
+ } else {
+ const k = ev.tags.find(t => ["e", "a", "i"].includes(t[0]));
+ return k?.[1];
+ }
}
/**
diff --git a/packages/system/src/event-kind.ts b/packages/system/src/event-kind.ts
index 8bbbbaee..f30ce316 100644
--- a/packages/system/src/event-kind.ts
+++ b/packages/system/src/event-kind.ts
@@ -12,12 +12,16 @@ const enum EventKind {
SimpleChatMessage = 9, // NIP-29
SealedRumor = 13, // NIP-59
ChatRumor = 14, // NIP-24
+ Photo = 20, // NIP-68
+ Video = 21, // NIP-71
+ ShortVideo = 22, // NIP-71
PublicChatChannel = 40, // NIP-28
PublicChatMetadata = 41, // NIP-28
PublicChatMessage = 42, // NIP-28
PublicChatMuteMessage = 43, // NIP-28
PublicChatMuteUser = 44, // NIP-28
SnortSubscriptions = 1000, // NIP-XX
+ Comment = 1111, // NIP-22
Polls = 6969, // NIP-69
GiftWrap = 1059, // NIP-59
FileHeader = 1063, // NIP-94
diff --git a/packages/system/src/event-publisher.ts b/packages/system/src/event-publisher.ts
index a39fc50c..fb143596 100644
--- a/packages/system/src/event-publisher.ts
+++ b/packages/system/src/event-publisher.ts
@@ -25,6 +25,7 @@ import { EventBuilder } from "./event-builder";
import { findTag } from "./utils";
import { Nip7Signer } from "./impl/nip7";
import { Nip10 } from "./impl/nip10";
+import { Nip22 } from "./impl/nip22";
type EventBuilderHook = (ev: EventBuilder) => EventBuilder;
@@ -195,12 +196,19 @@ export class EventPublisher {
/**
* Reply to a note
+ *
+ * Replies to kind 1 notes are kind 1, otherwise kind 1111
*/
async reply(replyTo: TaggedNostrEvent, msg: string, fnExtra?: EventBuilderHook) {
- const eb = this.#eb(EventKind.TextNote);
+ const kind = replyTo.kind === EventKind.TextNote ? EventKind.TextNote : EventKind.Comment;
+ const eb = this.#eb(kind);
eb.content(msg);
- Nip10.replyTo(replyTo, eb);
+ if (kind === EventKind.TextNote) {
+ Nip10.replyTo(replyTo, eb);
+ } else {
+ Nip22.replyTo(replyTo, eb);
+ }
eb.processContent();
fnExtra?.(eb);
return await this.#sign(eb);
@@ -211,6 +219,7 @@ export class EventPublisher {
eb.content(content);
eb.tag(unwrap(NostrLink.fromEvent(evRef).toEventTag()));
eb.tag(["p", evRef.pubkey]);
+ eb.tag(["k", evRef.kind.toString()]);
return await this.#sign(eb);
}
diff --git a/packages/system/src/impl/nip22.ts b/packages/system/src/impl/nip22.ts
new file mode 100644
index 00000000..a07d2b28
--- /dev/null
+++ b/packages/system/src/impl/nip22.ts
@@ -0,0 +1,35 @@
+import { findTag } from "../utils";
+import { EventBuilder, NostrEvent, NostrLink, NostrPrefix } from "../index";
+
+export class Nip22 {
+ /**
+ * Get the root scope tag (E/A/I) or
+ * create a root scope tag from the provided event
+ */
+ static rootScopeOf(other: NostrEvent) {
+ const linkOther = NostrLink.fromEvent(other);
+ return other.tags.find(t => ["E", "A", "I"].includes(t[0])) ?? linkOther.toEventTagNip22(true)!;
+ }
+
+ static replyTo(other: NostrEvent, eb: EventBuilder) {
+ const linkOther = NostrLink.fromEvent(other);
+ const rootScope = Nip22.rootScopeOf(other);
+ const rootKind = ["K", findTag(other, "K") ?? other.kind.toString()];
+ const rootAuthor = ["P", findTag(other, "P") ?? other.pubkey];
+
+ const replyScope = linkOther.toEventTagNip22(false);
+ const replyKind = ["k", other.kind.toString()];
+ const replyAuthor = ["p", other.pubkey];
+
+ if (rootScope === undefined || replyScope === undefined) {
+ throw new Error("RootScope or ReplyScope are undefined!");
+ }
+
+ eb.tag(rootScope);
+ eb.tag(rootKind);
+ eb.tag(rootAuthor);
+ eb.tag(replyScope);
+ eb.tag(replyKind);
+ eb.tag(replyAuthor);
+ }
+}
diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts
index 6e197793..13394f20 100644
--- a/packages/system/src/index.ts
+++ b/packages/system/src/index.ts
@@ -30,6 +30,7 @@ export * from "./encryption";
export * from "./impl/nip4";
export * from "./impl/nip7";
export * from "./impl/nip10";
+export * from "./impl/nip22";
export * from "./impl/nip44";
export * from "./impl/nip46";
export * from "./impl/nip57";
diff --git a/packages/system/src/nostr-link.ts b/packages/system/src/nostr-link.ts
index f96581b7..995d05c4 100644
--- a/packages/system/src/nostr-link.ts
+++ b/packages/system/src/nostr-link.ts
@@ -114,6 +114,32 @@ export class NostrLink implements ToNostrEventTag {
}
}
+ /**
+ * Create an event tag from this link as per NIP-22 (no marker position)
+ */
+ toEventTagNip22(root?: boolean) {
+ // emulate root flag by root marker
+ root ??= this.marker === "root";
+
+ const suffix: Array = [];
+ if (this.relays && this.relays.length > 0) {
+ suffix.push(this.relays[0]);
+ }
+ if (this.type === NostrPrefix.PublicKey || this.type === NostrPrefix.Profile) {
+ return [root ? "P" : "p", this.id, ...suffix];
+ } else if (this.type === NostrPrefix.Note || this.type === NostrPrefix.Event) {
+ if (this.author) {
+ if (suffix[0] === undefined) {
+ suffix.push(""); // empty relay hint
+ }
+ suffix.push(this.author);
+ }
+ return [root ? "E" : "e", this.id, ...suffix];
+ } else if (this.type === NostrPrefix.Address) {
+ return [root ? "A" : "a", `${this.kind}:${this.author}:${this.id}`, ...suffix];
+ }
+ }
+
matchesEvent(ev: NostrEvent) {
if (this.type === NostrPrefix.Address) {
const dTag = findTag(ev, "d");
@@ -194,12 +220,19 @@ export class NostrLink implements ToNostrEventTag {
static fromTag(tag: Array, author?: string, kind?: number) {
const relays = tag.length > 2 ? [tag[2]] : undefined;
switch (tag[0]) {
+ case "E": {
+ return new NostrLink(NostrPrefix.Event, tag[1], kind, author ?? tag[3], relays, "root");
+ }
case "e": {
return new NostrLink(NostrPrefix.Event, tag[1], kind, author ?? tag[4], relays, tag[3]);
}
case "p": {
return new NostrLink(NostrPrefix.Profile, tag[1], kind, author, relays);
}
+ case "A": {
+ const [kind, author, dTag] = tag[1].split(":");
+ return new NostrLink(NostrPrefix.Address, dTag, Number(kind), author, relays, "root");
+ }
case "a": {
const [kind, author, dTag] = tag[1].split(":");
return new NostrLink(NostrPrefix.Address, dTag, Number(kind), author, relays, tag[3]);
@@ -242,7 +275,7 @@ export class NostrLink implements ToNostrEventTag {
let relays = "relays" in ev ? ev.relays : undefined;
const eventRelays = removeUndefined(
ev.tags
- .filter(a => a[0] === "relays" || a[0] === "relay" || a[0] === "r")
+ .filter(a => a[0] === "relays" || a[0] === "relay" || (a[0] === "r" && ev.kind == EventKind.Relays))
.flatMap(a => a.slice(1).map(b => sanitizeRelayUrl(b))),
);
relays = appendDedupe(relays, eventRelays);
diff --git a/packages/system/src/utils.ts b/packages/system/src/utils.ts
index 51d393e9..34801ecd 100644
--- a/packages/system/src/utils.ts
+++ b/packages/system/src/utils.ts
@@ -1,6 +1,6 @@
import { equalProp } from "@snort/shared";
import { FlatReqFilter } from "./query-optimizer";
-import { IMeta, NostrEvent, ReqFilter } from "./nostr";
+import { NostrEvent, ReqFilter } from "./nostr";
export function findTag(e: NostrEvent, tag: string) {
const maybeTag = e.tags.find(evTag => {
@@ -9,6 +9,13 @@ export function findTag(e: NostrEvent, tag: string) {
return maybeTag && maybeTag[1];
}
+export function findWholeTag(e: NostrEvent, tag: string) {
+ const maybeTag = e.tags.find(evTag => {
+ return evTag[0] === tag;
+ });
+ return maybeTag;
+}
+
export function reqFilterEq(a: FlatReqFilter | ReqFilter, b: FlatReqFilter | ReqFilter): boolean {
return (
equalProp(a.ids, b.ids) &&