// SPDX-License-Identifier: AGPL-3.0-or-later import i18n from '@app/app/I18n'; import Channels from '@app/features/channel/state/Channels'; import type {UnicodeEmoji} from '@app/features/emoji/types/EmojiTypes'; import EmojiCatalog from '@app/features/expressions/utils/EmojiCatalog'; import {getSkinTonedSurrogate} from '@app/features/expressions/utils/SkinToneUtils'; import type {Message} from '@app/features/messaging/models/MessagingMessage'; import MessageReactions from '@app/features/messaging/state/MessageReactions'; import {getReactionKey, type ReactionEmoji} from '@app/features/messaging/utils/ReactionEmoji'; import * as ReactionUtils from '@app/features/messaging/utils/ReactionUtils'; import * as DisplayNameUtils from '@app/features/user/utils/DisplayNameUtils'; import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils'; import {msg, plural} from '@lingui/core/macro'; const REACTED_BY_DESCRIPTOR = msg({ message: '{reactorCount, plural, one {{emojiName} reacted by {reactors}} other {{emojiName} reacted by {reactors}}}', comment: 'Reaction tooltip. Preserve {emojiName}, {reactors}; they are inserted by code. {reactorCount} is how many people reacted, for verb agreement. Order the parts so the emoji never reads as the one doing the reacting.', }); const listFormatterCache = new Map(); function getReactorListFormatter(locale: string): Intl.ListFormat { let formatter = listFormatterCache.get(locale); if (!formatter) { formatter = new Intl.ListFormat(locale, {type: 'conjunction', style: 'long'}); listFormatterCache.set(locale, formatter); } return formatter; } export {getReactionKey, type ReactionEmoji}; export function getReactionTooltip(message: Message, emoji: ReactionEmoji) { const channel = Channels.getChannel(message.channelId); let guildId = message.guildId; if (channel !== null && channel !== undefined && channel.guildId !== undefined) { guildId = channel.guildId; } const users = MessageReactions.getReactions(message.id, emoji) .slice(0, 3) .map((user) => DisplayNameUtils.getNickname(user, guildId)); if (users.length === 0) { return ''; } const reaction = message.getReaction(emoji); const reactionCount = reaction === null || reaction === undefined ? 0 : reaction.count; const reactorCount = Math.max(users.length, reactionCount); const othersCount = Math.max(0, reactorCount - users.length); const emojiName = getEmojiNameWithColons(emoji); const parts: Array = [...users]; if (othersCount > 0) { parts.push(plural({count: othersCount}, {one: '# other', other: '# others'})); } const reactors = getReactorListFormatter(getCurrentLocale()).format(parts); return i18n._(REACTED_BY_DESCRIPTOR, {emojiName, reactors, reactorCount}); } const isCustomEmoji = (emoji: UnicodeEmoji | ReactionEmoji): emoji is ReactionEmoji => 'id' in emoji && emoji.id != null; export function toReactionEmoji(emoji: UnicodeEmoji | ReactionEmoji): ReactionEmoji { if (isCustomEmoji(emoji)) { const canonicalName = emoji.uniqueName === null || emoji.uniqueName === undefined ? emoji.name.replace(/~\d+$/, '') : emoji.uniqueName; if (canonicalName === emoji.name) { return emoji; } return {...emoji, name: canonicalName}; } return {name: getSkinTonedSurrogate(emoji)}; } export function emojiEquals(reactionEmoji: ReactionEmoji, emoji: UnicodeEmoji | ReactionEmoji) { return isCustomEmoji(emoji) ? emoji.id === reactionEmoji.id : reactionEmoji.id == null && emoji.name === reactionEmoji.name; } export function getEmojiName(emoji: ReactionEmoji): string { if (emoji.id != null) { return `:${emoji.name}:`; } const surrogate = EmojiCatalog.normalizeShortcodeToSurrogate(emoji.name); return EmojiCatalog.getSurrogateName(surrogate) || surrogate; } export function getEmojiNameWithColons(emoji: ReactionEmoji): string { if (emoji.id != null) { return `:${emoji.name}:`; } const surrogate = EmojiCatalog.normalizeShortcodeToSurrogate(emoji.name); const name = EmojiCatalog.getSurrogateName(surrogate); return name ? `:${name}:` : surrogate; } export interface EmojiURLParams { readonly emoji: ReactionEmoji; readonly isHovering?: boolean; readonly size?: number; readonly forceAnimate?: boolean; readonly enabled?: boolean; } export function useEmojiURL({ emoji, isHovering = false, size = 128, forceAnimate = false, enabled = true, }: EmojiURLParams): string | null { const url = ReactionUtils.useEmojiURL({emoji, isHovering, size, forceAnimate}); return enabled ? url : null; }