You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
93 lines
3.3 KiB
93 lines
3.3 KiB
import { DEFAULT_LIKE_REACTION_CONTENT } from '@/lib/like-reaction-emojis' |
|
import { replaceStandardEmojiShortcodesInContent } from '@/lib/emoji-content' |
|
import { isNip25ReactionKind } from '@/lib/event' |
|
import { getEmojiInfosFromEmojiTags } from '@/lib/tag' |
|
import { TEmoji } from '@/types' |
|
import { Event } from 'nostr-tools' |
|
|
|
/** Whole-string :shortcode: (NIP-style); matches content-patterns rules. */ |
|
const WHOLE_SHORTCODE = /^:([a-zA-Z0-9_\-][^:]{0,19}):$/ |
|
|
|
export type TReactionEmojiSync = |
|
| { mode: 'display'; value: TEmoji | string } |
|
| { mode: 'profile'; shortcode: string; placeholder: string } |
|
|
|
function findEmojiByShortcode(infos: readonly TEmoji[], shortcode: string): TEmoji | undefined { |
|
const lower = shortcode.toLowerCase() |
|
return infos.find((e) => e.shortcode === shortcode || e.shortcode.toLowerCase() === lower) |
|
} |
|
|
|
/** True when the reaction glyph must be resolved from the reactor’s NIP-30 inventory. */ |
|
export function reactionNeedsAuthorEmojiLookup(event: Event): boolean { |
|
return resolveReactionEmojiSync(event, 64).mode === 'profile' |
|
} |
|
|
|
/** Collect reactor pubkeys whose custom reaction emoji should be prefetched for feed/notification rows. */ |
|
export function collectReactionAuthorPubkeysForEmojiPrefetch( |
|
events: readonly Event[], |
|
candidates: Set<string> |
|
): void { |
|
for (const e of events) { |
|
if (!reactionNeedsAuthorEmojiLookup(e)) continue |
|
const pk = e.pubkey?.trim().toLowerCase() |
|
if (pk && /^[0-9a-f]{64}$/.test(pk)) candidates.add(pk) |
|
} |
|
} |
|
|
|
/** |
|
* Resolve reaction display without network: emoji tags on the reaction, standard :shortcode: → Unicode, |
|
* or defer to profile (reactor kind 0) for custom shortcodes. |
|
*/ |
|
export function resolveReactionEmojiSync(event: Event, maxRawLength: number): TReactionEmojiSync { |
|
if (!isNip25ReactionKind(event.kind)) { |
|
return { mode: 'display', value: '' } |
|
} |
|
|
|
const raw = event.content?.trim() ?? '' |
|
if (!raw) { |
|
return { mode: 'display', value: DEFAULT_LIKE_REACTION_CONTENT } |
|
} |
|
if (raw.length > maxRawLength) { |
|
return { mode: 'display', value: `${raw.slice(0, maxRawLength)}…` } |
|
} |
|
|
|
const fromReactionTags = getEmojiInfosFromEmojiTags(event.tags) |
|
const customShortcodes = fromReactionTags.map((e) => e.shortcode) |
|
|
|
if (/^https?:\/\//i.test(raw)) { |
|
const hit = fromReactionTags.find((e) => e.url === raw) |
|
if (hit) return { mode: 'display', value: hit } |
|
} |
|
|
|
if (fromReactionTags.length === 1 && raw === fromReactionTags[0].shortcode) { |
|
return { mode: 'display', value: fromReactionTags[0] } |
|
} |
|
|
|
const whole = raw.match(WHOLE_SHORTCODE) |
|
if (whole) { |
|
const shortcode = whole[1] |
|
const hit = findEmojiByShortcode(fromReactionTags, shortcode) |
|
if (hit) { |
|
return { mode: 'display', value: hit } |
|
} |
|
} |
|
|
|
const normalized = replaceStandardEmojiShortcodesInContent(raw, customShortcodes) |
|
if (normalized !== raw && !WHOLE_SHORTCODE.test(normalized.trim())) { |
|
return { mode: 'display', value: normalized.trim() } |
|
} |
|
|
|
if (whole) { |
|
return { mode: 'profile', shortcode: whole[1], placeholder: raw } |
|
} |
|
|
|
return { mode: 'display', value: raw } |
|
} |
|
|
|
/** Match a custom shortcode from a loaded author NIP-30 inventory. */ |
|
export function resolveAuthorEmojiForReactionShortcode( |
|
infos: readonly TEmoji[], |
|
shortcode: string |
|
): TEmoji | undefined { |
|
return findEmojiByShortcode(infos, shortcode) |
|
}
|
|
|