feat: add emoji set address as 4th param in NIP-30 emoji tags

NIP-30 allows an optional 4th tag parameter specifying the source emoji
set address (e.g. "30030:pubkey:identifier"). This threads that address
through the full emoji pipeline so it appears in posts, replies, reactions,
and zap requests.

- Add local blueprints.ts with patched NoteBlueprint, NoteReplyBlueprint,
  GroupMessageBlueprint, and ReactionBlueprint that emit the 4th param;
  marked TODO to revert once applesauce-common supports it upstream
- Add address? to EmojiWithAddress, EmojiTag, EmojiSearchResult, and
  EmojiTag in create-zap-request
- Store address in EmojiSearchService.addEmojiSet (30030:pubkey:identifier)
- Thread address through both editor serializers (MentionEditor, RichEditor)
  and the emoji node TipTap attributes
- Fix EmojiPickerDialog to pass address when calling onEmojiSelect and
  when re-indexing context emojis
- Update SendMessageOptions.emojiTags and sendReaction customEmoji param
  to use EmojiTag throughout the adapter chain

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gómez
2026-03-02 21:06:58 +01:00
parent 5c1d7c0c63
commit cffb981ad1
13 changed files with 260 additions and 33 deletions

View File

@@ -35,7 +35,7 @@ import { Kind1Renderer } from "./nostr/kinds";
import pool from "@/services/relay-pool";
import eventStore from "@/services/event-store";
import { EventFactory } from "applesauce-core/event-factory";
import { NoteBlueprint } from "applesauce-common/blueprints";
import { NoteBlueprint } from "@/lib/blueprints";
import { useGrimoire } from "@/core/state";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
import { normalizeRelayURL } from "@/lib/relay-url";
@@ -354,6 +354,7 @@ export function PostViewer({ windowId }: PostViewerProps = {}) {
emojis: emojiTags.map((e) => ({
shortcode: e.shortcode,
url: e.url,
address: e.address,
})),
});

View File

@@ -66,7 +66,7 @@ export function EmojiPickerDialog({
useEffect(() => {
if (contextEmojis.length > 0) {
for (const emoji of contextEmojis) {
service.addEmoji(emoji.shortcode, emoji.url, "context");
service.addEmoji(emoji.shortcode, emoji.url, "context", emoji.address);
}
}
}, [contextEmojis, service]);
@@ -143,6 +143,7 @@ export function EmojiPickerDialog({
onEmojiSelect(`:${result.shortcode}:`, {
shortcode: result.shortcode,
url: result.url,
address: result.address,
});
updateReactionHistory(`:${result.shortcode}:`);
}

View File

@@ -40,6 +40,8 @@ import { FilePasteHandler } from "./extensions/file-paste-handler";
export interface EmojiTag {
shortcode: string;
url: string;
/** NIP-30 optional 4th tag: "30030:pubkey:identifier" address of the emoji set */
address?: string;
}
/**
@@ -127,6 +129,14 @@ const EmojiMention = Mention.extend({
return { "data-source": attributes.source };
},
},
address: {
default: null,
parseHTML: (element) => element.getAttribute("data-address"),
renderHTML: (attributes) => {
if (!attributes.address) return {};
return { "data-address": attributes.address };
},
},
};
},
@@ -699,6 +709,7 @@ export const MentionEditor = forwardRef<
const shortcode = child.attrs?.id;
const url = child.attrs?.url;
const source = child.attrs?.source;
const address = child.attrs?.address;
if (source === "unicode" && url) {
// Unicode emoji - output the actual character
@@ -709,7 +720,11 @@ export const MentionEditor = forwardRef<
if (url && !seenEmojis.has(shortcode)) {
seenEmojis.add(shortcode);
emojiTags.push({ shortcode, url });
emojiTags.push({
shortcode,
url,
address: address ?? undefined,
});
}
}
} else if (child.type === "blobAttachment") {
@@ -893,6 +908,7 @@ export const MentionEditor = forwardRef<
label: props.shortcode,
url: props.url,
source: props.source,
address: props.address ?? null,
},
},
{ type: "text", text: " " },

View File

@@ -97,6 +97,14 @@ const EmojiMention = Mention.extend({
return { "data-source": attributes.source };
},
},
address: {
default: null,
parseHTML: (element) => element.getAttribute("data-address"),
renderHTML: (attributes) => {
if (!attributes.address) return {};
return { "data-address": attributes.address };
},
},
};
},
@@ -179,11 +187,11 @@ function serializeContent(editor: any): SerializedContent {
// Walk the document to collect emoji, blob, and address reference data
editor.state.doc.descendants((node: any) => {
if (node.type.name === "emoji") {
const { id, url, source } = node.attrs;
const { id, url, source, address } = node.attrs;
// Only add custom emojis (not unicode) and avoid duplicates
if (source !== "unicode" && !seenEmojis.has(id)) {
seenEmojis.add(id);
emojiTags.push({ shortcode: id, url });
emojiTags.push({ shortcode: id, url, address: address ?? undefined });
}
} else if (node.type.name === "blobAttachment") {
const { url, sha256, mimeType, size, server } = node.attrs;
@@ -500,6 +508,7 @@ export const RichEditor = forwardRef<RichEditorHandle, RichEditorProps>(
label: props.shortcode,
url: props.url,
source: props.source,
address: props.address ?? null,
},
},
{ type: "text", text: " " },

View File

@@ -41,7 +41,7 @@ import { cn } from "@/lib/utils";
import { isAddressableKind } from "@/lib/nostr-kinds";
import { getSemanticAuthor } from "@/lib/semantic-author";
import { EventFactory } from "applesauce-core/event-factory";
import { ReactionBlueprint } from "applesauce-common/blueprints";
import { ReactionBlueprint } from "@/lib/blueprints";
import { publishEventToRelays } from "@/services/hub";
import { selectRelaysForInteraction } from "@/services/relay-selection";
import type { EmojiTag } from "@/lib/emoji-helpers";
@@ -548,7 +548,11 @@ export function BaseEventContainer({
factory.setSigner(signer);
const emojiArg = customEmoji
? { shortcode: customEmoji.shortcode, url: customEmoji.url }
? {
shortcode: customEmoji.shortcode,
url: customEmoji.url,
address: customEmoji.address,
}
: emoji;
const draft = await factory.create(ReactionBlueprint, event, emojiArg);