feat: support rendering kind 20,21,22
feat: reply to non-text-note as kind 1111
This commit is contained in:
2
nap.yaml
2
nap.yaml
@ -8,4 +8,4 @@ repository: "https://github.com/v0l/snort"
|
||||
license: "MIT"
|
||||
tags:
|
||||
- "social"
|
||||
- "twitter"
|
||||
- "twitter"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
35
packages/system/src/impl/nip22.ts
Normal file
35
packages/system/src/impl/nip22.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
@ -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);
|
||||
|
@ -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) &&
|
||||
|
Reference in New Issue
Block a user