feat: support rendering kind 20,21,22

feat: reply to non-text-note as kind 1111
This commit is contained in:
2025-02-27 15:29:28 +00:00
parent 6977f80652
commit aaa4b0de97
12 changed files with 146 additions and 22 deletions

View File

@ -8,4 +8,4 @@ repository: "https://github.com/v0l/snort"
license: "MIT"
tags:
- "social"
- "twitter"
- "twitter"

View File

@ -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

View File

@ -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 = <ZapGoal ev={ev} />;
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 = <Note {...props} />;
break;
}
case EventKind.LongFormTextNote:
content = (
<LongFormText

View File

@ -32,7 +32,14 @@ const defaultOptions = {
showContextMenu: true,
};
const canRenderAsTextNote = [EventKind.TextNote, EventKind.Polls];
const canRenderAsTextNote = [
EventKind.TextNote,
EventKind.Polls,
EventKind.Photo,
EventKind.Video,
EventKind.ShortVideo,
EventKind.Comment,
];
const translationCache = new LRUCache<string, NoteTranslation>({ maxSize: 300 });
export function Note(props: NoteProps) {

View File

@ -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));
}
}
}

View File

@ -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];
}
}
/**

View File

@ -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

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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";

View File

@ -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<string> = [];
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<string>, 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);

View File

@ -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) &&