mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-07-25 22:37:43 +02:00
feat: Add collection reference to custom emoji tags
When using custom emoji from an emoji set (kind 30030), the emoji tag now includes an optional 4th element with the collection coordinate: ["emoji", "shortcode", "url", "30030:pubkey:identifier"] This enables: - Attribution: knowing which emoji set an emoji came from - Discovery: users can look up and subscribe to emoji sets Changes: - Add `collection` field to EmojiSearchResult and EmojiTag interfaces - Store full coordinate when indexing emoji from emoji sets - Propagate collection through editor serialization - Include collection in emoji tags for messages and reactions - Update all chat adapters (NIP-29, NIP-C7, NIP-53)
This commit is contained in:
@@ -138,6 +138,7 @@ export function EmojiPickerDialog({
|
||||
onEmojiSelect(`:${result.shortcode}:`, {
|
||||
shortcode: result.shortcode,
|
||||
url: result.url,
|
||||
collection: result.collection,
|
||||
});
|
||||
recordCustomUsage(result.shortcode, result.url);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ import { nip19 } from "nostr-tools";
|
||||
export interface EmojiTag {
|
||||
shortcode: string;
|
||||
url: string;
|
||||
/** Optional reference to the emoji set: "30030:pubkey:identifier" */
|
||||
collection?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -603,6 +605,7 @@ export const MentionEditor = forwardRef<
|
||||
const shortcode = child.attrs?.id;
|
||||
const url = child.attrs?.url;
|
||||
const source = child.attrs?.source;
|
||||
const collection = child.attrs?.collection;
|
||||
|
||||
if (source === "unicode" && url) {
|
||||
// Unicode emoji - output the actual character
|
||||
@@ -613,7 +616,7 @@ export const MentionEditor = forwardRef<
|
||||
|
||||
if (url && !seenEmojis.has(shortcode)) {
|
||||
seenEmojis.add(shortcode);
|
||||
emojiTags.push({ shortcode, url });
|
||||
emojiTags.push({ shortcode, url, collection });
|
||||
}
|
||||
}
|
||||
} else if (child.type === "blobAttachment") {
|
||||
@@ -761,6 +764,7 @@ export const MentionEditor = forwardRef<
|
||||
label: props.shortcode,
|
||||
url: props.url,
|
||||
source: props.source,
|
||||
collection: props.collection, // Reference to emoji set
|
||||
},
|
||||
},
|
||||
{ type: "text", text: " " },
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
CreateConversationParams,
|
||||
} from "@/types/chat";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import type { EmojiTag } from "@/lib/emoji-helpers";
|
||||
import type {
|
||||
ChatAction,
|
||||
ChatActionContext,
|
||||
@@ -40,7 +41,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[];
|
||||
}
|
||||
@@ -137,7 +138,7 @@ export abstract class ChatProtocolAdapter {
|
||||
conversation: Conversation,
|
||||
messageId: string,
|
||||
emoji: string,
|
||||
customEmoji?: { shortcode: string; url: string },
|
||||
customEmoji?: EmojiTag,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@ import { map, first, toArray } from "rxjs/operators";
|
||||
import type { Filter } from "nostr-tools";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { ChatProtocolAdapter, type SendMessageOptions } from "./base-adapter";
|
||||
import type { EmojiTag } from "@/lib/emoji-helpers";
|
||||
import type {
|
||||
Conversation,
|
||||
Message,
|
||||
@@ -465,7 +466,11 @@ export class Nip29Adapter extends ChatProtocolAdapter {
|
||||
// Add NIP-30 emoji tags
|
||||
if (options?.emojiTags) {
|
||||
for (const emoji of options.emojiTags) {
|
||||
tags.push(["emoji", emoji.shortcode, emoji.url]);
|
||||
const emojiTag: string[] = ["emoji", emoji.shortcode, emoji.url];
|
||||
if (emoji.collection) {
|
||||
emojiTag.push(emoji.collection);
|
||||
}
|
||||
tags.push(emojiTag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,7 +500,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;
|
||||
@@ -523,7 +528,15 @@ export class Nip29Adapter extends ChatProtocolAdapter {
|
||||
|
||||
// Add NIP-30 custom emoji tag if provided
|
||||
if (customEmoji) {
|
||||
tags.push(["emoji", customEmoji.shortcode, customEmoji.url]);
|
||||
const emojiTag: string[] = [
|
||||
"emoji",
|
||||
customEmoji.shortcode,
|
||||
customEmoji.url,
|
||||
];
|
||||
if (customEmoji.collection) {
|
||||
emojiTag.push(customEmoji.collection);
|
||||
}
|
||||
tags.push(emojiTag);
|
||||
}
|
||||
|
||||
// Use kind 7 for reactions
|
||||
|
||||
@@ -3,6 +3,7 @@ import { map, first, toArray } from "rxjs/operators";
|
||||
import type { Filter } from "nostr-tools";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { ChatProtocolAdapter, type SendMessageOptions } from "./base-adapter";
|
||||
import type { EmojiTag } from "@/lib/emoji-helpers";
|
||||
import type {
|
||||
Conversation,
|
||||
Message,
|
||||
@@ -446,7 +447,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]);
|
||||
const emojiTag: string[] = ["emoji", emoji.shortcode, emoji.url];
|
||||
if (emoji.collection) {
|
||||
emojiTag.push(emoji.collection);
|
||||
}
|
||||
tags.push(emojiTag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,7 +481,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;
|
||||
@@ -523,7 +528,15 @@ export class Nip53Adapter extends ChatProtocolAdapter {
|
||||
|
||||
// Add NIP-30 custom emoji tag if provided
|
||||
if (customEmoji) {
|
||||
tags.push(["emoji", customEmoji.shortcode, customEmoji.url]);
|
||||
const emojiTag: string[] = [
|
||||
"emoji",
|
||||
customEmoji.shortcode,
|
||||
customEmoji.url,
|
||||
];
|
||||
if (customEmoji.collection) {
|
||||
emojiTag.push(customEmoji.collection);
|
||||
}
|
||||
tags.push(emojiTag);
|
||||
}
|
||||
|
||||
// Use kind 7 for reactions
|
||||
|
||||
@@ -3,6 +3,7 @@ import { map, first } from "rxjs/operators";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import type { Filter } from "nostr-tools";
|
||||
import { ChatProtocolAdapter, type SendMessageOptions } from "./base-adapter";
|
||||
import type { EmojiTag } from "@/lib/emoji-helpers";
|
||||
import type {
|
||||
Conversation,
|
||||
Message,
|
||||
@@ -238,7 +239,11 @@ export class NipC7Adapter extends ChatProtocolAdapter {
|
||||
// Add NIP-30 emoji tags
|
||||
if (options?.emojiTags) {
|
||||
for (const emoji of options.emojiTags) {
|
||||
tags.push(["emoji", emoji.shortcode, emoji.url]);
|
||||
const emojiTag: string[] = ["emoji", emoji.shortcode, emoji.url];
|
||||
if (emoji.collection) {
|
||||
emojiTag.push(emoji.collection);
|
||||
}
|
||||
tags.push(emojiTag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +259,7 @@ export class NipC7Adapter 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;
|
||||
@@ -282,7 +287,15 @@ export class NipC7Adapter extends ChatProtocolAdapter {
|
||||
|
||||
// Add NIP-30 custom emoji tag if provided
|
||||
if (customEmoji) {
|
||||
tags.push(["emoji", customEmoji.shortcode, customEmoji.url]);
|
||||
const emojiTag: string[] = [
|
||||
"emoji",
|
||||
customEmoji.shortcode,
|
||||
customEmoji.url,
|
||||
];
|
||||
if (customEmoji.collection) {
|
||||
emojiTag.push(customEmoji.collection);
|
||||
}
|
||||
tags.push(emojiTag);
|
||||
}
|
||||
|
||||
// Use kind 7 for reactions
|
||||
|
||||
@@ -13,6 +13,8 @@ export const EMOJI_SHORTCODE_REGEX = /^:([a-zA-Z0-9_-]+):$/;
|
||||
export interface EmojiTag {
|
||||
shortcode: string;
|
||||
url: string;
|
||||
/** Optional reference to the emoji set: "30030:pubkey:identifier" */
|
||||
collection?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,7 +26,8 @@ const EmojiTagsSymbol = Symbol("emojiTags");
|
||||
* Extract and cache emoji tags from an event
|
||||
* Uses applesauce's symbol-based caching to avoid recomputation
|
||||
*
|
||||
* Emoji tags format: ["emoji", "shortcode", "url"]
|
||||
* Emoji tags format: ["emoji", "shortcode", "url", "collection?"]
|
||||
* where collection is optional: "30030:pubkey:identifier"
|
||||
*/
|
||||
export function getEmojiTags(event: NostrEvent): EmojiTag[] {
|
||||
return getOrComputeCachedValue(event, EmojiTagsSymbol, () =>
|
||||
@@ -33,6 +36,7 @@ export function getEmojiTags(event: NostrEvent): EmojiTag[] {
|
||||
.map((tag) => ({
|
||||
shortcode: tag[1],
|
||||
url: tag[2],
|
||||
collection: tag[3], // Optional 4th element
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface EmojiSearchResult {
|
||||
url: string;
|
||||
/** Source of the emoji: "unicode", "user", "set:<identifier>", or "context" */
|
||||
source: string;
|
||||
/** For emoji from sets: the full coordinate "30030:pubkey:identifier" */
|
||||
collection?: string;
|
||||
}
|
||||
|
||||
export class EmojiSearchService {
|
||||
@@ -24,11 +26,16 @@ export class EmojiSearchService {
|
||||
|
||||
/**
|
||||
* Add a single emoji to the search index
|
||||
* @param shortcode - The emoji shortcode (without colons)
|
||||
* @param url - The URL to the emoji image (or emoji char for unicode)
|
||||
* @param source - Source type: "unicode", "user", "set:<identifier>", or "context"
|
||||
* @param collection - For emoji from sets: the full coordinate "30030:pubkey:identifier"
|
||||
*/
|
||||
async addEmoji(
|
||||
shortcode: string,
|
||||
url: string,
|
||||
source: string = "custom",
|
||||
collection?: string,
|
||||
): Promise<void> {
|
||||
// Normalize shortcode (lowercase, no colons)
|
||||
const normalized = shortcode.toLowerCase().replace(/^:|:$/g, "");
|
||||
@@ -43,6 +50,7 @@ export class EmojiSearchService {
|
||||
shortcode: normalized,
|
||||
url,
|
||||
source,
|
||||
collection,
|
||||
};
|
||||
|
||||
this.emojis.set(normalized, emoji);
|
||||
@@ -59,8 +67,16 @@ export class EmojiSearchService {
|
||||
event.tags.find((t) => t[0] === "d")?.[1] || "unnamed-set";
|
||||
const emojis = getEmojiTags(event);
|
||||
|
||||
// Build the full coordinate for collection reference
|
||||
const collection = `30030:${event.pubkey}:${identifier}`;
|
||||
|
||||
for (const emoji of emojis) {
|
||||
await this.addEmoji(emoji.shortcode, emoji.url, `set:${identifier}`);
|
||||
await this.addEmoji(
|
||||
emoji.shortcode,
|
||||
emoji.url,
|
||||
`set:${identifier}`,
|
||||
collection,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user