Files
fluxer/fluxer_app/src/features/messaging/utils/MessageReactionUtils.ts
T

115 lines
4.4 KiB
TypeScript

// 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<string, Intl.ListFormat>();
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<string> = [...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;
}