signal-desktop/ts/components/emoji/lib.ts

235 lines
5.8 KiB
TypeScript
Raw Normal View History

2023-01-03 11:55:46 -08:00
// Copyright 2019 Signal Messenger, LLC
2020-10-30 15:34:04 -05:00
// SPDX-License-Identifier: AGPL-3.0-only
// Camelcase disabled due to emoji-datasource using snake_case
/* eslint-disable camelcase */
2019-05-24 16:58:27 -07:00
import {
compact,
flatMap,
groupBy,
keyBy,
map,
mapValues,
sortBy,
} from 'lodash';
import { getOwn } from '../../util/getOwn';
2025-03-26 12:35:32 -07:00
import {
EMOJI_SKIN_TONE_TO_KEY,
EmojiSkinTone,
KEY_TO_EMOJI_SKIN_TONE,
} from '../fun/data/emojis';
import { strictAssert } from '../../util/assert';
2019-05-24 16:58:27 -07:00
2024-08-14 16:58:01 -07:00
// Import emoji-datasource dynamically to avoid costly typechecking.
// eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-var-requires
const untypedData = require('emoji-datasource' as string);
2019-05-24 16:58:27 -07:00
export const skinTones = ['1F3FB', '1F3FC', '1F3FD', '1F3FE', '1F3FF'];
export type SkinToneKey = '1F3FB' | '1F3FC' | '1F3FD' | '1F3FE' | '1F3FF';
type EmojiSkinVariation = {
unified: string;
non_qualified: null;
image: string;
sheet_x: number;
sheet_y: number;
added_in: string;
has_img_apple: boolean;
has_img_google: boolean;
has_img_twitter: boolean;
has_img_emojione: boolean;
has_img_facebook: boolean;
has_img_messenger: boolean;
};
2019-05-24 16:58:27 -07:00
export type EmojiData = {
name: string;
unified: string;
non_qualified: string | null;
docomo: string | null;
au: string | null;
softbank: string | null;
google: string | null;
image: string;
sheet_x: number;
sheet_y: number;
short_name: string;
short_names: Array<string>;
text: string | null;
texts: Array<string> | null;
category: string;
sort_order: number;
added_in: string;
has_img_apple: boolean;
has_img_google: boolean;
has_img_twitter: boolean;
has_img_facebook: boolean;
skin_variations?: {
[key: string]: EmojiSkinVariation;
2019-05-24 16:58:27 -07:00
};
};
2024-08-14 16:58:01 -07:00
export const data = (untypedData as Array<EmojiData>)
2020-01-23 13:17:06 -08:00
.filter(emoji => emoji.has_img_apple)
.map(emoji =>
// Why this weird map?
// the emoji dataset has two separate categories for Emotions and People
// yet in our UI we display these as a single merged category. In order
// for the emojis to be sorted properly we're manually incrementing the
// sort_order for the People & Body emojis so that they fall below the
// Smiley & Emotions category.
emoji.category === 'People & Body'
? { ...emoji, sort_order: emoji.sort_order + 1000 }
: emoji
);
const dataByShortName = keyBy(data, 'short_name');
2020-01-17 17:23:19 -05:00
const dataByEmoji: { [key: string]: EmojiData } = {};
2019-05-24 16:58:27 -07:00
export const dataByCategory = mapValues(
groupBy(data, ({ category }) => {
if (category === 'Activities') {
return 'activity';
}
if (category === 'Animals & Nature') {
return 'animal';
}
if (category === 'Flags') {
return 'flag';
}
if (category === 'Food & Drink') {
return 'food';
}
if (category === 'Objects') {
return 'object';
}
if (category === 'Travel & Places') {
return 'travel';
}
2020-01-23 13:17:06 -08:00
if (category === 'Smileys & Emotion') {
return 'emoji';
}
if (category === 'People & Body') {
2019-05-24 16:58:27 -07:00
return 'emoji';
}
if (category === 'Symbols') {
return 'symbol';
}
return 'misc';
}),
arr => sortBy(arr, 'sort_order')
);
export function getEmojiData(
2019-05-24 16:58:27 -07:00
shortName: keyof typeof dataByShortName,
2025-03-26 12:35:32 -07:00
emojiSkinToneDefault: EmojiSkinTone
): EmojiData | EmojiSkinVariation {
2019-05-24 16:58:27 -07:00
const base = dataByShortName[shortName];
2025-03-26 12:35:32 -07:00
const variation = EMOJI_SKIN_TONE_TO_KEY.get(emojiSkinToneDefault);
2019-05-24 16:58:27 -07:00
2025-03-26 12:35:32 -07:00
if (variation != null && base.skin_variations) {
2020-01-23 13:17:06 -08:00
if (base.skin_variations[variation]) {
return base.skin_variations[variation];
}
// For emojis that have two people in them which can have diff skin tones
// the Map is of SkinTone-SkinTone. If we don't find the correct skin tone
// in the list of variations then we assume it is one of those double skin
// emojis and we default to both people having same skin.
return base.skin_variations[`${variation}-${variation}`];
2019-05-24 16:58:27 -07:00
}
return base;
}
2019-05-24 16:58:27 -07:00
const shortNames = new Set([
...map(data, 'short_name'),
...compact<string>(flatMap(data, 'short_names')),
]);
export function isShortName(name: string): boolean {
2019-05-24 16:58:27 -07:00
return shortNames.has(name);
}
export function unifiedToEmoji(unified: string): string {
2019-05-24 16:58:27 -07:00
return unified
.split('-')
.map(c => String.fromCodePoint(parseInt(c, 16)))
.join('');
}
export function convertShortNameToData(
shortName: string,
2025-03-26 12:35:32 -07:00
skinTone: EmojiSkinTone
): EmojiData | undefined {
2019-05-24 16:58:27 -07:00
const base = dataByShortName[shortName];
if (!base) {
return undefined;
2019-05-24 16:58:27 -07:00
}
2025-03-26 12:35:32 -07:00
if (skinTone !== EmojiSkinTone.None && base.skin_variations != null) {
const toneKey = EMOJI_SKIN_TONE_TO_KEY.get(skinTone);
strictAssert(toneKey, `Missing key for skin tone: ${skinTone}`);
const variation =
base.skin_variations[toneKey] ??
base.skin_variations[`${toneKey}-${toneKey}`];
2019-05-24 16:58:27 -07:00
if (variation) {
return {
...base,
...variation,
};
2019-05-24 16:58:27 -07:00
}
}
return base;
}
export function convertShortName(
shortName: string,
2025-03-26 12:35:32 -07:00
skinTone: EmojiSkinTone
): string {
const emojiData = convertShortNameToData(shortName, skinTone);
if (!emojiData) {
return '';
}
return unifiedToEmoji(emojiData.unified);
2019-05-24 16:58:27 -07:00
}
2020-10-02 13:05:09 -07:00
export function emojiToData(emoji: string): EmojiData | undefined {
2020-11-04 13:56:49 -06:00
return getOwn(dataByEmoji, emoji);
2020-10-02 13:05:09 -07:00
}
data.forEach(emoji => {
2025-03-26 12:35:32 -07:00
const { short_name, short_names, skin_variations } = emoji;
if (short_names) {
short_names.forEach(name => {
dataByShortName[name] = emoji;
});
}
2025-03-26 12:35:32 -07:00
dataByEmoji[convertShortName(short_name, EmojiSkinTone.None)] = emoji;
if (skin_variations) {
2025-03-26 12:35:32 -07:00
Object.entries(skin_variations).forEach(([tone]) => {
const emojiSkinTone = KEY_TO_EMOJI_SKIN_TONE.get(tone);
if (emojiSkinTone != null) {
dataByEmoji[convertShortName(short_name, emojiSkinTone)] = emoji;
}
});
}
});