mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-08 14:37:04 +02:00
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:
@@ -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,
|
||||
})),
|
||||
});
|
||||
|
||||
|
||||
@@ -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}:`);
|
||||
}
|
||||
|
||||
@@ -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: " " },
|
||||
|
||||
@@ -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: " " },
|
||||
|
||||
@@ -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);
|
||||
|
||||
180
src/lib/blueprints.ts
Normal file
180
src/lib/blueprints.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Local copies of applesauce-common blueprints with NIP-30 emoji set address support.
|
||||
*
|
||||
* The upstream Emoji type only has { shortcode, url }. NIP-30 allows an optional 4th
|
||||
* tag parameter for the emoji set address (e.g. "30030:pubkey:identifier"). These
|
||||
* blueprints add that via a local `EmojiWithAddress` type and a custom `includeEmojis`
|
||||
* operation. A patch upstream will be submitted once stable.
|
||||
*
|
||||
* TODO: Once applesauce-common supports the emoji set address natively, remove this
|
||||
* file and revert all imports back to `applesauce-common/blueprints`.
|
||||
*/
|
||||
|
||||
import { blueprint } from "applesauce-core/event-factory";
|
||||
import { kinds } from "applesauce-core/helpers/event";
|
||||
import {
|
||||
setShortTextContent,
|
||||
type TextContentOptions,
|
||||
} from "applesauce-core/operations/content";
|
||||
import {
|
||||
setMetaTags,
|
||||
type MetaTagOptions,
|
||||
} from "applesauce-core/operations/event";
|
||||
import type { EventOperation } from "applesauce-core/event-factory";
|
||||
import {
|
||||
setZapSplit,
|
||||
type ZapOptions,
|
||||
} from "applesauce-common/operations/zap-split";
|
||||
import {
|
||||
includePubkeyNotificationTags,
|
||||
setThreadParent,
|
||||
} from "applesauce-common/operations/note";
|
||||
import {
|
||||
addPreviousRefs,
|
||||
setGroupPointer,
|
||||
} from "applesauce-common/operations/group";
|
||||
import {
|
||||
setReaction,
|
||||
setReactionParent,
|
||||
} from "applesauce-common/operations/reaction";
|
||||
import {
|
||||
GROUP_MESSAGE_KIND,
|
||||
type GroupPointer,
|
||||
} from "applesauce-common/helpers/groups";
|
||||
import type { NostrEvent } from "nostr-tools";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extended emoji type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type EmojiWithAddress = {
|
||||
shortcode: string;
|
||||
url: string;
|
||||
/** NIP-30 optional 4th tag: the "30030:pubkey:identifier" address of the set */
|
||||
address?: string;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom includeEmojis operation that writes the 4th address param when present
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Expressions = {
|
||||
emoji: /:([a-zA-Z0-9_-]+):/g,
|
||||
};
|
||||
|
||||
function includeEmojisWithAddress(emojis: EmojiWithAddress[]): EventOperation {
|
||||
return (draft, ctx) => {
|
||||
// Merge context emojis (upstream compat) with explicitly passed emojis
|
||||
const all: EmojiWithAddress[] = [
|
||||
...(ctx.emojis ?? []).map((e) => ({
|
||||
shortcode: e.shortcode,
|
||||
url: e.url,
|
||||
})),
|
||||
...emojis,
|
||||
];
|
||||
const emojiTags = Array.from(
|
||||
draft.content.matchAll(Expressions.emoji),
|
||||
([, name]) => {
|
||||
const emoji = all.find((e) => e.shortcode === name);
|
||||
if (!emoji?.url) return null;
|
||||
return emoji.address
|
||||
? ["emoji", emoji.shortcode, emoji.url, emoji.address]
|
||||
: ["emoji", emoji.shortcode, emoji.url];
|
||||
},
|
||||
).filter((tag): tag is string[] => tag !== null);
|
||||
return { ...draft, tags: [...draft.tags, ...emojiTags] };
|
||||
};
|
||||
}
|
||||
|
||||
// TextContentOptions extended with our emoji type
|
||||
export type TextContentOptionsWithAddress = Omit<
|
||||
TextContentOptions,
|
||||
"emojis"
|
||||
> & {
|
||||
emojis?: EmojiWithAddress[];
|
||||
};
|
||||
|
||||
// MetaTagOptions extended (MetaTagOptions doesn't use emojis, but we carry the same pattern)
|
||||
export type NoteBlueprintOptions = TextContentOptionsWithAddress &
|
||||
MetaTagOptions &
|
||||
ZapOptions;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NoteBlueprint
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function NoteBlueprint(content: string, options?: NoteBlueprintOptions) {
|
||||
return blueprint(
|
||||
kinds.ShortTextNote,
|
||||
// set text content (without emoji — we handle it ourselves)
|
||||
setShortTextContent(content, { ...options, emojis: undefined }),
|
||||
options?.emojis ? includeEmojisWithAddress(options.emojis) : undefined,
|
||||
setZapSplit(options),
|
||||
setMetaTags(options),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NoteReplyBlueprint
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function NoteReplyBlueprint(
|
||||
parent: NostrEvent,
|
||||
content: string,
|
||||
options?: TextContentOptionsWithAddress,
|
||||
) {
|
||||
if (parent.kind !== kinds.ShortTextNote)
|
||||
throw new Error(
|
||||
"Kind 1 replies should only be used to reply to kind 1 notes",
|
||||
);
|
||||
return blueprint(
|
||||
kinds.ShortTextNote,
|
||||
setThreadParent(parent),
|
||||
includePubkeyNotificationTags(parent),
|
||||
setShortTextContent(content, { ...options, emojis: undefined }),
|
||||
options?.emojis ? includeEmojisWithAddress(options.emojis) : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GroupMessageBlueprint
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type GroupMessageOptions = TextContentOptionsWithAddress & {
|
||||
previous?: NostrEvent[];
|
||||
};
|
||||
|
||||
export function GroupMessageBlueprint(
|
||||
group: GroupPointer,
|
||||
content: string,
|
||||
options?: GroupMessageOptions,
|
||||
) {
|
||||
return blueprint(
|
||||
GROUP_MESSAGE_KIND,
|
||||
setGroupPointer(group),
|
||||
options?.previous ? addPreviousRefs(options.previous) : undefined,
|
||||
setShortTextContent(content, { ...options, emojis: undefined }),
|
||||
options?.emojis ? includeEmojisWithAddress(options.emojis) : undefined,
|
||||
setMetaTags(options),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ReactionBlueprint
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function ReactionBlueprint(
|
||||
event: NostrEvent,
|
||||
emoji: string | EmojiWithAddress = "+",
|
||||
) {
|
||||
return blueprint(
|
||||
kinds.Reaction,
|
||||
setReaction(
|
||||
typeof emoji === "string"
|
||||
? emoji
|
||||
: { shortcode: emoji.shortcode, url: emoji.url },
|
||||
),
|
||||
setReactionParent(event),
|
||||
typeof emoji !== "string" ? includeEmojisWithAddress([emoji]) : undefined,
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
CreateConversationParams,
|
||||
} from "@/types/chat";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import type { EmojiTag } from "@/lib/emoji-helpers";
|
||||
import type {
|
||||
ChatAction,
|
||||
ChatActionContext,
|
||||
@@ -71,7 +72,7 @@ export interface SendMessageOptions {
|
||||
/** Event ID being replied to */
|
||||
replyTo?: string;
|
||||
/** NIP-30 custom emoji tags */
|
||||
emojiTags?: Array<{ shortcode: string; url: string }>;
|
||||
emojiTags?: EmojiTag[];
|
||||
/** Blob attachments for imeta tags (NIP-92) */
|
||||
blobAttachments?: BlobAttachmentMeta[];
|
||||
}
|
||||
@@ -168,7 +169,7 @@ export abstract class ChatProtocolAdapter {
|
||||
conversation: Conversation,
|
||||
messageId: string,
|
||||
emoji: string,
|
||||
customEmoji?: { shortcode: string; url: string },
|
||||
customEmoji?: EmojiTag,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
Participant,
|
||||
} from "@/types/chat";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import type { EmojiTag } from "@/lib/emoji-helpers";
|
||||
import eventStore from "@/services/event-store";
|
||||
import pool from "@/services/relay-pool";
|
||||
import { publishEventToRelays } from "@/services/hub";
|
||||
@@ -26,10 +27,7 @@ import { mergeRelaySets } from "applesauce-core/helpers";
|
||||
import { getOutboxes } from "applesauce-core/helpers/mailboxes";
|
||||
import { getEventPointerFromETag } from "applesauce-core/helpers/pointers";
|
||||
import { EventFactory } from "applesauce-core/event-factory";
|
||||
import {
|
||||
NoteReplyBlueprint,
|
||||
ReactionBlueprint,
|
||||
} from "applesauce-common/blueprints";
|
||||
import { NoteReplyBlueprint, ReactionBlueprint } from "@/lib/blueprints";
|
||||
import { getNip10References } from "applesauce-common/helpers";
|
||||
import {
|
||||
getZapAmount,
|
||||
@@ -396,6 +394,7 @@ export class Nip10Adapter extends ChatProtocolAdapter {
|
||||
emojis: options?.emojiTags?.map((e) => ({
|
||||
shortcode: e.shortcode,
|
||||
url: e.url,
|
||||
address: e.address,
|
||||
})),
|
||||
},
|
||||
);
|
||||
@@ -425,7 +424,7 @@ export class Nip10Adapter extends ChatProtocolAdapter {
|
||||
conversation: Conversation,
|
||||
messageId: string,
|
||||
emoji: string,
|
||||
customEmoji?: { shortcode: string; url: string },
|
||||
customEmoji?: EmojiTag,
|
||||
): Promise<void> {
|
||||
const activePubkey = accountManager.active$.value?.pubkey;
|
||||
const activeSigner = accountManager.active$.value?.signer;
|
||||
@@ -450,9 +449,7 @@ export class Nip10Adapter extends ChatProtocolAdapter {
|
||||
factory.setSigner(activeSigner);
|
||||
|
||||
// Use ReactionBlueprint - auto-handles e-tag, k-tag, p-tag, custom emoji
|
||||
const emojiArg = customEmoji
|
||||
? { shortcode: customEmoji.shortcode, url: customEmoji.url }
|
||||
: emoji;
|
||||
const emojiArg = customEmoji ?? emoji;
|
||||
|
||||
const draft = await factory.create(
|
||||
ReactionBlueprint,
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
ParticipantRole,
|
||||
} from "@/types/chat";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import type { EmojiTag } from "@/lib/emoji-helpers";
|
||||
import type { ChatAction, GetActionsOptions } from "@/types/chat-actions";
|
||||
import eventStore from "@/services/event-store";
|
||||
import pool from "@/services/relay-pool";
|
||||
@@ -29,10 +30,7 @@ import { getEventPointerFromETag } from "applesauce-core/helpers/pointers";
|
||||
import { mergeRelaySets } from "applesauce-core/helpers";
|
||||
import { normalizeRelayURL } from "@/lib/relay-url";
|
||||
import { EventFactory } from "applesauce-core/event-factory";
|
||||
import {
|
||||
GroupMessageBlueprint,
|
||||
ReactionBlueprint,
|
||||
} from "applesauce-common/blueprints";
|
||||
import { GroupMessageBlueprint, ReactionBlueprint } from "@/lib/blueprints";
|
||||
import { resolveGroupMetadata } from "@/lib/chat/group-metadata-helpers";
|
||||
|
||||
/**
|
||||
@@ -465,6 +463,7 @@ export class Nip29Adapter extends ChatProtocolAdapter {
|
||||
emojis: options?.emojiTags?.map((e) => ({
|
||||
shortcode: e.shortcode,
|
||||
url: e.url,
|
||||
address: e.address,
|
||||
})),
|
||||
},
|
||||
);
|
||||
@@ -508,7 +507,7 @@ export class Nip29Adapter extends ChatProtocolAdapter {
|
||||
conversation: Conversation,
|
||||
messageId: string,
|
||||
emoji: string,
|
||||
customEmoji?: { shortcode: string; url: string },
|
||||
customEmoji?: EmojiTag,
|
||||
): Promise<void> {
|
||||
const activePubkey = accountManager.active$.value?.pubkey;
|
||||
const activeSigner = accountManager.active$.value?.signer;
|
||||
@@ -538,9 +537,7 @@ export class Nip29Adapter extends ChatProtocolAdapter {
|
||||
factory.setSigner(activeSigner);
|
||||
|
||||
// Use ReactionBlueprint - auto-handles e-tag, k-tag, p-tag, custom emoji
|
||||
const emojiArg = customEmoji
|
||||
? { shortcode: customEmoji.shortcode, url: customEmoji.url }
|
||||
: emoji;
|
||||
const emojiArg = customEmoji ?? emoji;
|
||||
|
||||
const draft = await factory.create(
|
||||
ReactionBlueprint,
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
ParticipantRole,
|
||||
} from "@/types/chat";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import type { EmojiTag } from "@/lib/emoji-helpers";
|
||||
import eventStore from "@/services/event-store";
|
||||
import pool from "@/services/relay-pool";
|
||||
import { publishEventToRelays } from "@/services/hub";
|
||||
@@ -42,7 +43,7 @@ import {
|
||||
isValidZap,
|
||||
} from "applesauce-common/helpers/zap";
|
||||
import { EventFactory } from "applesauce-core/event-factory";
|
||||
import { ReactionBlueprint } from "applesauce-common/blueprints";
|
||||
import { ReactionBlueprint } from "@/lib/blueprints";
|
||||
|
||||
/**
|
||||
* NIP-53 Adapter - Live Activity Chat
|
||||
@@ -466,7 +467,11 @@ export class Nip53Adapter extends ChatProtocolAdapter {
|
||||
// Add NIP-30 emoji tags
|
||||
if (options?.emojiTags) {
|
||||
for (const emoji of options.emojiTags) {
|
||||
tags.push(["emoji", emoji.shortcode, emoji.url]);
|
||||
tags.push(
|
||||
emoji.address
|
||||
? ["emoji", emoji.shortcode, emoji.url, emoji.address]
|
||||
: ["emoji", emoji.shortcode, emoji.url],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,7 +501,7 @@ export class Nip53Adapter extends ChatProtocolAdapter {
|
||||
conversation: Conversation,
|
||||
messageId: string,
|
||||
emoji: string,
|
||||
customEmoji?: { shortcode: string; url: string },
|
||||
customEmoji?: EmojiTag,
|
||||
): Promise<void> {
|
||||
const activePubkey = accountManager.active$.value?.pubkey;
|
||||
const activeSigner = accountManager.active$.value?.signer;
|
||||
@@ -545,9 +550,7 @@ export class Nip53Adapter extends ChatProtocolAdapter {
|
||||
factory.setSigner(activeSigner);
|
||||
|
||||
// Use ReactionBlueprint - auto-handles e-tag, k-tag, p-tag, custom emoji
|
||||
const emojiArg = customEmoji
|
||||
? { shortcode: customEmoji.shortcode, url: customEmoji.url }
|
||||
: emoji;
|
||||
const emojiArg = customEmoji ?? emoji;
|
||||
|
||||
const draft = await factory.create(
|
||||
ReactionBlueprint,
|
||||
|
||||
@@ -12,6 +12,8 @@ import { selectZapRelays } from "./zap-relay-selection";
|
||||
export interface EmojiTag {
|
||||
shortcode: string;
|
||||
url: string;
|
||||
/** NIP-30 optional 4th tag: "30030:pubkey:identifier" address of the emoji set */
|
||||
address?: string;
|
||||
}
|
||||
|
||||
export interface ZapRequestParams {
|
||||
@@ -124,7 +126,11 @@ export async function createZapRequest(
|
||||
// Add NIP-30 emoji tags
|
||||
if (params.emojiTags) {
|
||||
for (const emoji of params.emojiTags) {
|
||||
tags.push(["emoji", emoji.shortcode, emoji.url]);
|
||||
tags.push(
|
||||
emoji.address
|
||||
? ["emoji", emoji.shortcode, emoji.url, emoji.address]
|
||||
: ["emoji", emoji.shortcode, emoji.url],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ export const EMOJI_SHORTCODE_REGEX = /^:([a-zA-Z0-9_-]+):$/;
|
||||
export interface EmojiTag {
|
||||
shortcode: string;
|
||||
url: string;
|
||||
/** NIP-30 optional 4th tag: "30030:pubkey:identifier" address of the emoji set */
|
||||
address?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface EmojiSearchResult {
|
||||
url: string;
|
||||
/** Source of the emoji: "unicode", "user", "set:<identifier>", or "context" */
|
||||
source: string;
|
||||
/** NIP-30 optional 4th tag: "30030:pubkey:identifier" address of the emoji set */
|
||||
address?: string;
|
||||
}
|
||||
|
||||
export class EmojiSearchService {
|
||||
@@ -29,6 +31,7 @@ export class EmojiSearchService {
|
||||
shortcode: string,
|
||||
url: string,
|
||||
source: string = "custom",
|
||||
address?: string,
|
||||
): Promise<void> {
|
||||
// Normalize shortcode (lowercase, no colons)
|
||||
const normalized = shortcode.toLowerCase().replace(/^:|:$/g, "");
|
||||
@@ -43,6 +46,7 @@ export class EmojiSearchService {
|
||||
shortcode: normalized,
|
||||
url,
|
||||
source,
|
||||
address,
|
||||
};
|
||||
|
||||
this.emojis.set(normalized, emoji);
|
||||
@@ -57,10 +61,16 @@ export class EmojiSearchService {
|
||||
|
||||
const identifier =
|
||||
event.tags.find((t) => t[0] === "d")?.[1] || "unnamed-set";
|
||||
const address = `30030:${event.pubkey}:${identifier}`;
|
||||
const emojis = getEmojiTags(event);
|
||||
|
||||
for (const emoji of emojis) {
|
||||
await this.addEmoji(emoji.shortcode, emoji.url, `set:${identifier}`);
|
||||
await this.addEmoji(
|
||||
emoji.shortcode,
|
||||
emoji.url,
|
||||
`set:${identifier}`,
|
||||
address,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user