Init new emoji data/api
This commit is contained in:
parent
71fe87c611
commit
592e1b4476
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,6 +5,7 @@ coverage/*
|
||||
build/curve25519_compiled.js
|
||||
build/compact-locales
|
||||
build/*.policy
|
||||
build/emoji-data.json
|
||||
stylesheets/*.css.map
|
||||
/dist
|
||||
.DS_Store
|
||||
|
||||
@ -231,8 +231,7 @@ const STD_PACKAGES = new Set([
|
||||
'danger',
|
||||
'debug',
|
||||
'direction',
|
||||
'emoji-datasource',
|
||||
'emoji-regex',
|
||||
'emoji-regex-xs',
|
||||
'eslint',
|
||||
'eslint-plugin-better-tailwindcss',
|
||||
'filesize',
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
# Generated files
|
||||
build/**/*.js
|
||||
build/**/*.json
|
||||
app/**/*.js
|
||||
config/local-*.json
|
||||
config/local.json
|
||||
@ -30,6 +31,7 @@ packages/lame/lame-*/
|
||||
danger/node_modules/**
|
||||
sticker-creator/node_modules/**
|
||||
components/**
|
||||
scripts/emoji-datasource/emoji-datasource.json
|
||||
|
||||
# Assets
|
||||
/images/
|
||||
|
||||
@ -30,13 +30,13 @@ import { Environment, setEnvironment } from '../ts/environment.std.ts';
|
||||
import { parseUnknown } from '../ts/util/schemas.std.ts';
|
||||
import { LocaleEmojiListSchema } from '../ts/types/emoji.std.ts';
|
||||
import { FunProvider } from '../ts/components/fun/FunProvider.dom.tsx';
|
||||
import { EmojiSkinTone } from '../ts/components/fun/data/emojis.std.ts';
|
||||
import { MOCK_GIFS_PAGINATED_ONE_PAGE } from '../ts/test-helpers/funPickerMocks.dom.tsx';
|
||||
import { NavTab } from '../ts/types/Nav.std.ts';
|
||||
|
||||
import type { FunEmojiSelection } from '../ts/components/fun/panels/FunPanelEmojis.dom.tsx';
|
||||
import type { FunGifSelection } from '../ts/components/fun/panels/FunPanelGifs.dom.tsx';
|
||||
import type { FunStickerSelection } from '../ts/components/fun/panels/FunPanelStickers.dom.tsx';
|
||||
import { Emoji } from '../ts/axo/emoji.std.ts';
|
||||
|
||||
setEnvironment(Environment.Development, true);
|
||||
|
||||
@ -258,7 +258,7 @@ function withFunProvider(Story, context) {
|
||||
recentEmojis={[]}
|
||||
recentStickers={[]}
|
||||
recentGifs={[]}
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
emojiSkinToneDefault={Emoji.SkinTone.None}
|
||||
onEmojiSkinToneDefaultChange={noop}
|
||||
installedStickerPacks={[]}
|
||||
showStickerPickerHint={false}
|
||||
|
||||
@ -5897,11 +5897,11 @@ Signal Desktop makes use of the following open source projects.
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## emoji-datasource
|
||||
## emoji-regex-xs
|
||||
|
||||
The MIT License (MIT)
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2013 Cal Henderson
|
||||
Copyright (c) 2025 Steven Levithan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -5921,29 +5921,6 @@ Signal Desktop makes use of the following open source projects.
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## emoji-regex
|
||||
|
||||
Copyright Mathias Bynens <https://mathiasbynens.be/>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## enhanced-resolve
|
||||
|
||||
Copyright JS Foundation and other contributors
|
||||
|
||||
@ -12,6 +12,7 @@ import { SignalService as Proto } from '../ts/protobuf/index.std.ts';
|
||||
import { parseUnknown } from '../ts/util/schemas.std.ts';
|
||||
import { utf16ToEmoji } from '../ts/util/utf16ToEmoji.node.ts';
|
||||
import { getAppRootDir } from '../ts/util/appRootDir.main.ts';
|
||||
import { Emoji } from '../ts/axo/emoji.std.ts';
|
||||
|
||||
const MANIFEST_PATH = join(getAppRootDir(), 'build', 'jumbomoji.json');
|
||||
|
||||
@ -28,7 +29,7 @@ type SheetCacheEntry = Map<string, Uint8Array<ArrayBuffer>>;
|
||||
|
||||
export class EmojiService {
|
||||
readonly #resourceService: OptionalResourceService;
|
||||
readonly #emojiMap = new Map<string, EmojiEntryType>();
|
||||
readonly #emojiMap = new Map<Emoji.Variant, EmojiEntryType>();
|
||||
|
||||
readonly #sheetCache = new LRUCache<string, SheetCacheEntry>({
|
||||
// Each sheet is roughly 500kb
|
||||
@ -44,16 +45,21 @@ export class EmojiService {
|
||||
protocol.handle('emoji', async req => {
|
||||
const url = new URL(req.url);
|
||||
const emoji = url.searchParams.get('emoji');
|
||||
if (!emoji) {
|
||||
if (emoji == null || !Emoji.isEmoji(emoji)) {
|
||||
return new Response('invalid', { status: 400 });
|
||||
}
|
||||
|
||||
return this.#fetch(emoji);
|
||||
return this.#fetch(Emoji.ignorePreferredSkinTone(emoji));
|
||||
});
|
||||
|
||||
for (const [sheet, emojiList] of Object.entries(manifest)) {
|
||||
for (const utf16 of emojiList) {
|
||||
this.#emojiMap.set(utf16, { sheet, utf16 });
|
||||
if (Emoji.isEmoji(utf16)) {
|
||||
this.#emojiMap.set(Emoji.ignorePreferredSkinTone(utf16), {
|
||||
sheet,
|
||||
utf16,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -67,7 +73,7 @@ export class EmojiService {
|
||||
return new EmojiService(resourceService, manifest);
|
||||
}
|
||||
|
||||
async #fetch(emoji: string): Promise<Response> {
|
||||
async #fetch(emoji: Emoji.Variant): Promise<Response> {
|
||||
const entry = this.#emojiMap.get(emoji);
|
||||
if (!entry) {
|
||||
return new Response('entry not found', { status: 404 });
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import getEmojiRegex from 'emoji-regex';
|
||||
import getEmojiRegex from 'emoji-regex-xs';
|
||||
import type {
|
||||
MessageFormatElement,
|
||||
TagElement,
|
||||
|
||||
13
package.json
13
package.json
@ -19,7 +19,7 @@
|
||||
"postuninstall": "pnpm run build:acknowledgments",
|
||||
"start": "electron .",
|
||||
"generate": "run-s generate:phase-0 generate:phase-1",
|
||||
"generate:phase-0": "run-s build:protobuf build:rolldown:prod",
|
||||
"generate:phase-0": "run-s build:protobuf build:emoji-data build:rolldown:prod",
|
||||
"generate:phase-1": "run-p --aggregate-output --print-label build:icu-types build:compact-locales build:styles:prod get-expire-time build:policy-files",
|
||||
"build-release": "pnpm run build",
|
||||
"notarize": "echo 'No longer necessary'",
|
||||
@ -77,7 +77,7 @@
|
||||
"dev:styles:tailwind": "pnpm run build:styles:tailwind --watch",
|
||||
"dev:icu-types": "chokidar ./_locales/en/messages.json --initial --command \"pnpm run build:icu-types\"",
|
||||
"dev:protobuf": "chokidar ./protos/**/*.proto --command \"pnpm run build:protobuf\"",
|
||||
"build:storybook": "pnpm run build:protobuf && cross-env SIGNAL_ENV=storybook storybook build",
|
||||
"build:storybook": "pnpm run build:protobuf && pnpm run build:emoji-data && cross-env SIGNAL_ENV=storybook storybook build",
|
||||
"test:storybook": "pnpm run build:storybook && run-p --race test:storybook:*",
|
||||
"test:storybook:serve": "http-server storybook-static --port 6006 --silent",
|
||||
"test:storybook:test": "wait-on http://127.0.0.1:6006/ --timeout 5000 && test-storybook --testTimeout 60000",
|
||||
@ -86,6 +86,7 @@
|
||||
"build-linux": "run-s build:policy-files generate build:rolldown:prod && pnpm run build:release --publish=never",
|
||||
"build:acknowledgments": "node scripts/generate-acknowledgments.mjs",
|
||||
"build:dns-fallback": "node scripts/generate-dns-fallback.mjs",
|
||||
"build:emoji-data": "node scripts/generate-emoji-data.mjs",
|
||||
"build:icu-types": "node scripts/generate-icu-types.mjs",
|
||||
"build:compact-locales": "node scripts/generate-compact-locales.mjs",
|
||||
"build:tray-icons": "node scripts/generate-tray-icons.mjs",
|
||||
@ -121,7 +122,6 @@
|
||||
"@signalapp/ringrtc": "2.68.1",
|
||||
"@signalapp/sqlcipher": "3.2.1",
|
||||
"@signalapp/windows-ucv": "1.0.1",
|
||||
"emoji-datasource": "16.0.0",
|
||||
"google-libphonenumber": "3.2.44"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -237,7 +237,7 @@
|
||||
"electron": "41.5.0",
|
||||
"electron-builder": "26.0.14",
|
||||
"electron-mocha": "13.1.0",
|
||||
"emoji-regex": "10.6.0",
|
||||
"emoji-regex-xs": "2.0.1",
|
||||
"enhanced-resolve": "5.20.1",
|
||||
"enquirer": "2.4.1",
|
||||
"eslint": "10.1.0",
|
||||
@ -630,6 +630,7 @@
|
||||
"build/dns-fallback.json",
|
||||
"build/optional-resources.json",
|
||||
"build/jumbomoji.json",
|
||||
"build/emoji-data.json",
|
||||
"node_modules/**",
|
||||
"!node_modules/.cache",
|
||||
"!**/node_modules/**/{CHANGELOG.md,README.md,README,readme.md,readme,test,__tests__,tests,powered-test,example,examples,*.d.ts,*.d.ts.map,*.js.map,*.gypi,.snyk-*.flag,benchmark}",
|
||||
@ -637,10 +638,6 @@
|
||||
"!**/node_modules/.bin",
|
||||
"!**/node_modules/**/build/**",
|
||||
"!**/node_modules/**/prebuilds/**",
|
||||
"!node_modules/emoji-datasource/emoji_pretty.json",
|
||||
"!node_modules/emoji-datasource/img/**/*.png",
|
||||
"node_modules/emoji-datasource/categories.json",
|
||||
"node_modules/emoji-datasource/emoji.json",
|
||||
"node_modules/@signalapp/sqlcipher/prebuilds/${platform}-${arch}/*.node",
|
||||
"node_modules/@signalapp/libsignal-client/prebuilds/${platform}-${arch}/*.node",
|
||||
"!node_modules/@signalapp/ringrtc/scripts/*",
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@ -87,9 +87,6 @@ importers:
|
||||
'@signalapp/windows-ucv':
|
||||
specifier: 1.0.1
|
||||
version: 1.0.1
|
||||
emoji-datasource:
|
||||
specifier: 16.0.0
|
||||
version: 16.0.0
|
||||
google-libphonenumber:
|
||||
specifier: 3.2.44
|
||||
version: 3.2.44
|
||||
@ -430,9 +427,9 @@ importers:
|
||||
electron-mocha:
|
||||
specifier: 13.1.0
|
||||
version: 13.1.0
|
||||
emoji-regex:
|
||||
specifier: 10.6.0
|
||||
version: 10.6.0
|
||||
emoji-regex-xs:
|
||||
specifier: 2.0.1
|
||||
version: 2.0.1
|
||||
enhanced-resolve:
|
||||
specifier: 5.20.1
|
||||
version: 5.20.1
|
||||
@ -6156,11 +6153,9 @@ packages:
|
||||
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
emoji-datasource@16.0.0:
|
||||
resolution: {integrity: sha512-/qHKqK5Nr3+8zhgO6kHmF43Fm5C8HNn0AaFRIpgw8HF3+uF0Vfc8jgLI1ZQS5ba1vBzksS8NBCjHejwLb2D/Sg==}
|
||||
|
||||
emoji-regex@10.6.0:
|
||||
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
||||
emoji-regex-xs@2.0.1:
|
||||
resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
emoji-regex@7.0.3:
|
||||
resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==}
|
||||
@ -16400,9 +16395,7 @@ snapshots:
|
||||
|
||||
emittery@0.13.1: {}
|
||||
|
||||
emoji-datasource@16.0.0: {}
|
||||
|
||||
emoji-regex@10.6.0: {}
|
||||
emoji-regex-xs@2.0.1: {}
|
||||
|
||||
emoji-regex@7.0.3: {}
|
||||
|
||||
|
||||
@ -24,7 +24,6 @@ const external = [
|
||||
'sass',
|
||||
|
||||
// Large libraries (3.7mb total)
|
||||
'emoji-datasource',
|
||||
'google-libphonenumber',
|
||||
|
||||
// Imported, but not used in production builds
|
||||
|
||||
1
scripts/emoji-datasource/emoji-datasource.json
Normal file
1
scripts/emoji-datasource/emoji-datasource.json
Normal file
File diff suppressed because one or more lines are too long
307
scripts/generate-emoji-data.mjs
Normal file
307
scripts/generate-emoji-data.mjs
Normal file
@ -0,0 +1,307 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
|
||||
// Keep this data in sync with:
|
||||
// https://raw.githubusercontent.com/greyson-signal/emoji-data/457ad4f7a09699ec940b149c8f4e76382bb0aadf/emoji.json
|
||||
import SRC_EMOJIS from './emoji-datasource/emoji-datasource.json' with { type: 'json' };
|
||||
import getEmojiRegex from 'emoji-regex-xs';
|
||||
import { join } from 'node:path';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { gzip } from 'node:zlib';
|
||||
import { promisify } from 'node:util';
|
||||
import { assert } from './utils/assert.mjs';
|
||||
|
||||
const PRETTY = process.argv.includes('--pretty');
|
||||
|
||||
const gzipAsync = promisify(gzip);
|
||||
const EMOJI_REGEX = getEmojiRegex();
|
||||
|
||||
const ROOT_DIR = join(import.meta.dirname, '..');
|
||||
const EMOJI_DATA_PATH = join(ROOT_DIR, 'build', 'emoji-data.json');
|
||||
|
||||
/**
|
||||
* @typedef {string & { Emoji: never }} Emoji
|
||||
*
|
||||
* @typedef {(
|
||||
* | 'SMILIES_AND_PEOPLE'
|
||||
* | 'ANIMALS_AND_NATURE'
|
||||
* | 'FOOD_AND_DRINK'
|
||||
* | 'TRAVEL_AND_PLACES'
|
||||
* | 'ACTIVITIES'
|
||||
* | 'OBJECTS'
|
||||
* | 'SYMBOLS'
|
||||
* | 'FLAGS'
|
||||
* )} Category
|
||||
*
|
||||
* @typedef {(
|
||||
* | '1F3FB'
|
||||
* | '1F3FC'
|
||||
* | '1F3FD'
|
||||
* | '1F3FE'
|
||||
* | '1F3FF'
|
||||
* )} SkinTone
|
||||
*
|
||||
* @typedef {Record<SkinTone, Emoji>} SkinTones
|
||||
*
|
||||
* @typedef {{
|
||||
* shortName: string;
|
||||
* shortNameAlts?: Array<string>;
|
||||
* emoticon?: string;
|
||||
* emoticonAlts?: Array<string>;
|
||||
* skinTones?: SkinTones;
|
||||
* }} EmojiInfo
|
||||
*
|
||||
* @typedef {Record<Category, Array<Emoji>>} Categories
|
||||
* @typedef {Record<Emoji, EmojiInfo>} Emojis
|
||||
* @typedef {Record<Emoji, Emoji>} Parents
|
||||
*
|
||||
* @typedef {{ categories: Categories, emojis: Emojis, parents: Parents }} EmojiData
|
||||
*/
|
||||
|
||||
/** @satisfies {Record<string, Category | null>} */
|
||||
const CATEGORY_NAME_MAP = {
|
||||
'Smileys & Emotion': 'SMILIES_AND_PEOPLE', // merged
|
||||
'People & Body': 'SMILIES_AND_PEOPLE', // merged
|
||||
Component: null, // dropped
|
||||
'Animals & Nature': 'ANIMALS_AND_NATURE',
|
||||
'Food & Drink': 'FOOD_AND_DRINK',
|
||||
'Travel & Places': 'TRAVEL_AND_PLACES',
|
||||
Activities: 'ACTIVITIES',
|
||||
Objects: 'OBJECTS',
|
||||
Symbols: 'SYMBOLS',
|
||||
Flags: 'FLAGS',
|
||||
};
|
||||
|
||||
/** @type {{ [K in SkinTone]: K }} */
|
||||
const SKIN_TONES = {
|
||||
'1F3FB': '1F3FB',
|
||||
'1F3FC': '1F3FC',
|
||||
'1F3FD': '1F3FD',
|
||||
'1F3FE': '1F3FE',
|
||||
'1F3FF': '1F3FF',
|
||||
};
|
||||
|
||||
const KNOWN_MISSING_APPLE_IMG = new Set([
|
||||
'FEMALE SIGN',
|
||||
'MALE SIGN',
|
||||
'MEDICAL SYMBOL',
|
||||
]);
|
||||
|
||||
const DEPRECATED_EMOJI = new Set([
|
||||
/**
|
||||
* 2022 - Family Emoji Redesign: Gender Inclusive Variants
|
||||
* https://www.unicode.org/L2/L2023/23029-family-emoji.pdf
|
||||
* https://www.unicode.org/L2/L2022/22276-family-emoji-guidelines.pdf
|
||||
*/
|
||||
'FAMILY: MAN, BOY, BOY',
|
||||
'FAMILY: MAN, BOY',
|
||||
'FAMILY: MAN, GIRL, BOY',
|
||||
'FAMILY: MAN, GIRL, GIRL',
|
||||
'FAMILY: MAN, GIRL',
|
||||
'FAMILY: MAN, MAN, BOY',
|
||||
'FAMILY: MAN, MAN, BOY, BOY',
|
||||
'FAMILY: MAN, MAN, GIRL',
|
||||
'FAMILY: MAN, MAN, GIRL, BOY',
|
||||
'FAMILY: MAN, MAN, GIRL, GIRL',
|
||||
'FAMILY: MAN, WOMAN, BOY',
|
||||
'FAMILY: MAN, WOMAN, BOY, BOY',
|
||||
'FAMILY: MAN, WOMAN, GIRL',
|
||||
'FAMILY: MAN, WOMAN, GIRL, BOY',
|
||||
'FAMILY: MAN, WOMAN, GIRL, GIRL',
|
||||
'FAMILY: WOMAN, BOY, BOY',
|
||||
'FAMILY: WOMAN, BOY',
|
||||
'FAMILY: WOMAN, GIRL, BOY',
|
||||
'FAMILY: WOMAN, GIRL, GIRL',
|
||||
'FAMILY: WOMAN, GIRL',
|
||||
'FAMILY: WOMAN, WOMAN, BOY',
|
||||
'FAMILY: WOMAN, WOMAN, BOY, BOY',
|
||||
'FAMILY: WOMAN, WOMAN, GIRL',
|
||||
'FAMILY: WOMAN, WOMAN, GIRL, BOY',
|
||||
'FAMILY: WOMAN, WOMAN, GIRL, GIRL',
|
||||
]);
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @returns {input is Emoji}
|
||||
*/
|
||||
function isEmoji(input) {
|
||||
const match = input.match(EMOJI_REGEX);
|
||||
return match != null && match[0] === input;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @returns {input is keyof typeof CATEGORY_NAME_MAP}
|
||||
*/
|
||||
function isCategoryName(input) {
|
||||
return Object.hasOwn(CATEGORY_NAME_MAP, input);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Emoji} emoji
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isDeprecated(emoji) {
|
||||
return DEPRECATED_EMOJI.has(emoji);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} unified
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeUnified(unified) {
|
||||
return unified
|
||||
.split('-')
|
||||
.map(char => String.fromCodePoint(Number.parseInt(char, 16)))
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Partial<SkinTones>} skinTones
|
||||
* @returns {skinTones is SkinTones}
|
||||
*/
|
||||
function hasAllSkinTones(skinTones) {
|
||||
return Object.values(SKIN_TONES).every(skinTone => {
|
||||
return Object.hasOwn(skinTones, skinTone) && skinTones[skinTone] != null;
|
||||
});
|
||||
}
|
||||
|
||||
const SRC_EMOJIS_SORTED = SRC_EMOJIS.toSorted((a, b) => {
|
||||
return a.sort_order - b.sort_order;
|
||||
});
|
||||
|
||||
/** @type {Categories} */
|
||||
const categories = {
|
||||
SMILIES_AND_PEOPLE: [],
|
||||
ANIMALS_AND_NATURE: [],
|
||||
FOOD_AND_DRINK: [],
|
||||
TRAVEL_AND_PLACES: [],
|
||||
ACTIVITIES: [],
|
||||
OBJECTS: [],
|
||||
SYMBOLS: [],
|
||||
FLAGS: [],
|
||||
};
|
||||
/** @type {Emojis} */
|
||||
const emojis = {};
|
||||
/** @type {Parents} */
|
||||
const parents = {};
|
||||
|
||||
for (const emojiSrc of SRC_EMOJIS_SORTED) {
|
||||
const emojiName = emojiSrc.name;
|
||||
|
||||
const categoryName = emojiSrc.category;
|
||||
assert(isCategoryName(categoryName), `Unexpected category: ${categoryName}`);
|
||||
|
||||
const category = CATEGORY_NAME_MAP[categoryName];
|
||||
if (category == null) {
|
||||
continue; // drop components
|
||||
}
|
||||
|
||||
assert(
|
||||
emojiSrc.has_img_apple === !KNOWN_MISSING_APPLE_IMG.has(emojiName),
|
||||
'Unexpected mismatch between has_img_apple and KNOWN_MISSING_APPLE_IMG'
|
||||
);
|
||||
if (!emojiSrc.has_img_apple) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const emoji = encodeUnified(emojiSrc.unified);
|
||||
assert(isEmoji(emoji), 'Unexpected invalid emoji');
|
||||
|
||||
if (isDeprecated(emoji)) {
|
||||
continue; // drop deprecated emoji
|
||||
}
|
||||
|
||||
/** @type {EmojiInfo} */
|
||||
const info = {
|
||||
shortName: emojiSrc.short_name,
|
||||
};
|
||||
|
||||
const shortNameAlts = new Set(emojiSrc.short_names);
|
||||
shortNameAlts.delete(emojiSrc.short_name);
|
||||
if (shortNameAlts.size !== 0) {
|
||||
info.shortNameAlts = Array.from(shortNameAlts);
|
||||
}
|
||||
|
||||
if (emojiSrc.text != null) {
|
||||
info.emoticon = emojiSrc.text;
|
||||
}
|
||||
|
||||
const emoticonAlts = new Set(emojiSrc.texts);
|
||||
if (emojiSrc.text != null) {
|
||||
emoticonAlts.delete(emojiSrc.text);
|
||||
}
|
||||
if (emoticonAlts.size !== 0) {
|
||||
info.emoticonAlts = Array.from(emoticonAlts);
|
||||
}
|
||||
|
||||
if (emojiSrc.skin_variations != null) {
|
||||
/** @type {Partial<Record<SkinTone, Emoji>>} */
|
||||
const skinTones = {};
|
||||
|
||||
for (const skinTone of Object.values(SKIN_TONES)) {
|
||||
const match =
|
||||
emojiSrc.skin_variations[skinTone] ??
|
||||
emojiSrc.skin_variations[`${skinTone}-${skinTone}`];
|
||||
|
||||
if (match == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const variant = encodeUnified(match.unified);
|
||||
|
||||
assert(match.has_img_apple, 'Unexpected missing apple image');
|
||||
assert(isEmoji(variant), 'Unexpected invalid variant');
|
||||
assert(!isDeprecated(variant), 'Unexpected deprecated variant');
|
||||
|
||||
skinTones[skinTone] = variant;
|
||||
}
|
||||
|
||||
assert(hasAllSkinTones(skinTones), 'Expected to have all skin tones');
|
||||
info.skinTones = skinTones;
|
||||
|
||||
for (const skinTone of Object.values(SKIN_TONES)) {
|
||||
const variant = info.skinTones[skinTone];
|
||||
parents[variant] = emoji;
|
||||
}
|
||||
}
|
||||
|
||||
categories[category] ??= [];
|
||||
categories[category].push(emoji);
|
||||
|
||||
emojis[emoji] = info;
|
||||
}
|
||||
|
||||
/** @type {EmojiData} */
|
||||
const emojiData = {
|
||||
categories,
|
||||
emojis,
|
||||
parents,
|
||||
};
|
||||
|
||||
const bytesFormat = new Intl.NumberFormat('en', {
|
||||
style: 'unit',
|
||||
unit: 'byte',
|
||||
unitDisplay: 'narrow',
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {unknown} json
|
||||
*/
|
||||
async function logGzipSize(name, json) {
|
||||
const src = `${JSON.stringify(json)}\n`;
|
||||
const srcBytes = bytesFormat.format(Buffer.byteLength(src, 'utf8'));
|
||||
const gzipped = await gzipAsync(`${JSON.stringify(json)}\n`);
|
||||
const gzipBytes = bytesFormat.format(gzipped.length);
|
||||
console.log(`${name}: ${srcBytes} (${gzipBytes} gzip)`);
|
||||
}
|
||||
|
||||
await logGzipSize('before', SRC_EMOJIS);
|
||||
await logGzipSize('after', emojiData);
|
||||
|
||||
const json = PRETTY
|
||||
? JSON.stringify(emojiData, null, 2)
|
||||
: JSON.stringify(emojiData);
|
||||
await writeFile(EMOJI_DATA_PATH, `${json}\n`);
|
||||
813
ts/axo/emoji.std.ts
Normal file
813
ts/axo/emoji.std.ts
Normal file
@ -0,0 +1,813 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import getEmojiRegex from 'emoji-regex-xs';
|
||||
import Fuse from 'fuse.js';
|
||||
import type { FuseOptionKey, IFuseOptions } from 'fuse.js';
|
||||
import LOOSE_EMOJI_DATA from '../../build/emoji-data.json' with { type: 'json' };
|
||||
import { assert } from './_internal/assert.std.tsx';
|
||||
|
||||
function lazy<T>(fn: () => T): () => T {
|
||||
let cached: T;
|
||||
return () => {
|
||||
cached ??= fn();
|
||||
return cached;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Data
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
type ParentInfo = Readonly<{
|
||||
shortName: Emoji.ShortName;
|
||||
shortNameAlts?: ReadonlyArray<Emoji.ShortName>;
|
||||
emoticon?: string;
|
||||
emoticonAlts?: ReadonlyArray<string>;
|
||||
skinTones?: Readonly<Record<Emoji.SkinTone, Emoji.SkinToneVariant>>;
|
||||
}>;
|
||||
|
||||
type EmojiData = Readonly<{
|
||||
categories: Record<Emoji.Category, Array<Emoji.Parent>>;
|
||||
emojis: Readonly<Record<Emoji.Parent, ParentInfo>>;
|
||||
parents: Readonly<Record<Emoji.SkinToneVariant, Emoji.Parent>>;
|
||||
}>;
|
||||
|
||||
const EMOJI_DATA: EmojiData = {
|
||||
// TypeScript only complains about this one for some reason
|
||||
categories: LOOSE_EMOJI_DATA.categories as EmojiData['categories'],
|
||||
emojis: LOOSE_EMOJI_DATA.emojis,
|
||||
parents: LOOSE_EMOJI_DATA.parents,
|
||||
};
|
||||
|
||||
function getParentInfo(parent: Emoji.Parent): ParentInfo {
|
||||
return assert(EMOJI_DATA.emojis[parent], `Missing "${parent}"`);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
function PARENT_AND_ONLY_VARIANT<const T extends string>(
|
||||
input: T
|
||||
): T & Emoji.Parent & Emoji.Variant {
|
||||
assert(Emoji.isEmoji(input));
|
||||
assert(Emoji.isParent(input));
|
||||
const info = getParentInfo(input);
|
||||
assert(info.skinTones == null);
|
||||
return input as T & Emoji.Parent & Emoji.Variant;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
function PARENT(input: string): Emoji.Parent {
|
||||
assert(Emoji.isEmoji(input));
|
||||
assert(Emoji.isParent(input));
|
||||
const info = getParentInfo(input);
|
||||
assert(info.skinTones != null);
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* String Normalization
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const EMOJI_COMPLETION_REGEX = /(?<!^)[\s,_-]+/gi;
|
||||
|
||||
/** @testexport For displaying in the ui */
|
||||
export function _toEmojiCompletionLabel(
|
||||
shortName: Emoji.ShortName
|
||||
): Emoji.CompletionLabel {
|
||||
return shortName
|
||||
.normalize()
|
||||
.replaceAll(EMOJI_COMPLETION_REGEX, '_')
|
||||
.toLowerCase() as Emoji.CompletionLabel;
|
||||
}
|
||||
|
||||
/** @testexport For matching in search utils */
|
||||
export function _toEmojiCompletionQuery(
|
||||
shortName: Emoji.ShortName
|
||||
): Emoji.CompletionQuery {
|
||||
return shortName
|
||||
.normalize('NFD')
|
||||
.replaceAll(/\p{Diacritic}/gu, '')
|
||||
.replaceAll(EMOJI_COMPLETION_REGEX, ' ')
|
||||
.toLowerCase() as Emoji.CompletionQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
type SearchIndexEntry = Readonly<{
|
||||
parent: Emoji.Parent;
|
||||
rank: number;
|
||||
completion: Emoji.CompletionQuery;
|
||||
completionAlts: ReadonlyArray<Emoji.CompletionQuery>;
|
||||
emoticon: string | null;
|
||||
emoticonAlts: ReadonlyArray<string>;
|
||||
}>;
|
||||
|
||||
const ALL_PARENTS_WITH_DEFAULT_RANK = lazy(() => {
|
||||
const parents = new Map<Emoji.Parent, number>();
|
||||
for (const category of Object.values(EMOJI_DATA.categories)) {
|
||||
for (const parent of category) {
|
||||
parents.set(parent, parents.size + 1);
|
||||
}
|
||||
}
|
||||
return parents;
|
||||
});
|
||||
|
||||
type EmojiSearchFuseKey = FuseOptionKey<SearchIndexEntry> & {
|
||||
name: keyof SearchIndexEntry;
|
||||
};
|
||||
|
||||
const EmojiSearchFuseKeys: Array<EmojiSearchFuseKey> = [
|
||||
{ name: 'completion', weight: 100 },
|
||||
{ name: 'completionAlts', weight: 1 },
|
||||
{ name: 'emoticon', weight: 50 },
|
||||
{ name: 'emoticonAlts', weight: 1 },
|
||||
];
|
||||
|
||||
const EmojiSearchFuseFuzzyOptions: IFuseOptions<SearchIndexEntry> = {
|
||||
shouldSort: false,
|
||||
threshold: 0.2,
|
||||
minMatchCharLength: 1,
|
||||
keys: EmojiSearchFuseKeys,
|
||||
includeScore: true,
|
||||
includeMatches: true,
|
||||
};
|
||||
|
||||
const EmojiSearchFuseExactOptions: IFuseOptions<SearchIndexEntry> = {
|
||||
shouldSort: false,
|
||||
threshold: 0,
|
||||
minMatchCharLength: 1,
|
||||
keys: EmojiSearchFuseKeys,
|
||||
includeScore: true,
|
||||
includeMatches: true,
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
type EmojiSearchFuses = Readonly<{
|
||||
size: number;
|
||||
getExact: () => Fuse<SearchIndexEntry>;
|
||||
getFuzzy: () => Fuse<SearchIndexEntry>;
|
||||
}>;
|
||||
|
||||
/** @internal */
|
||||
function createEmojiSearchFuses(
|
||||
searchIndex: ReadonlyArray<SearchIndexEntry>
|
||||
): EmojiSearchFuses {
|
||||
const size = searchIndex.length;
|
||||
const INDEX = lazy(() => Fuse.createIndex(EmojiSearchFuseKeys, searchIndex));
|
||||
return {
|
||||
size,
|
||||
getExact: lazy(
|
||||
() => new Fuse(searchIndex, EmojiSearchFuseExactOptions, INDEX())
|
||||
),
|
||||
getFuzzy: lazy(
|
||||
() => new Fuse(searchIndex, EmojiSearchFuseFuzzyOptions, INDEX())
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Localizer
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/** @internal */
|
||||
type EmojiLocalizer = Readonly<{
|
||||
getShortName: (parent: Emoji.Parent) => Emoji.ShortName;
|
||||
matchCompletion: (completion: Emoji.CompletionQuery) => Emoji.Parent | null;
|
||||
getSearchFuses: () => EmojiSearchFuses;
|
||||
}>;
|
||||
|
||||
/** @internal */
|
||||
const DEFAULT_ENGLISH_INDEXES = lazy(() => {
|
||||
const completionToParent = new Map<Emoji.CompletionQuery, Emoji.Parent>();
|
||||
const searchIndex: Array<SearchIndexEntry> = [];
|
||||
|
||||
for (const [parent, rank] of ALL_PARENTS_WITH_DEFAULT_RANK()) {
|
||||
const info = getParentInfo(parent);
|
||||
const shortName = info.shortName;
|
||||
const shortNameAlts = info.shortNameAlts ?? [];
|
||||
|
||||
const completion = _toEmojiCompletionQuery(shortName);
|
||||
const completionAlts = shortNameAlts.map(shortNameAlt => {
|
||||
return _toEmojiCompletionQuery(shortNameAlt);
|
||||
});
|
||||
|
||||
completionToParent.set(completion, parent);
|
||||
|
||||
searchIndex.push({
|
||||
parent,
|
||||
rank,
|
||||
completion,
|
||||
completionAlts,
|
||||
emoticon: info.emoticon ?? null,
|
||||
emoticonAlts: info.emoticonAlts ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
return { completionToParent, searchIndex };
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
const DEFAULT_ENGLISH_LOCALIZER: EmojiLocalizer = {
|
||||
getShortName(value) {
|
||||
return getParentInfo(value).shortName;
|
||||
},
|
||||
matchCompletion(completion) {
|
||||
return DEFAULT_ENGLISH_INDEXES().completionToParent.get(completion) ?? null;
|
||||
},
|
||||
getSearchFuses() {
|
||||
return createEmojiSearchFuses(DEFAULT_ENGLISH_INDEXES().searchIndex);
|
||||
},
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
let CURRENT_EMOJI_LOCALIZER: EmojiLocalizer | null = null;
|
||||
|
||||
/** @internal */
|
||||
function getLocalizer() {
|
||||
return CURRENT_EMOJI_LOCALIZER ?? DEFAULT_ENGLISH_LOCALIZER;
|
||||
}
|
||||
|
||||
/** @testexport */
|
||||
export function _hasEmojiLocalizer(): boolean {
|
||||
return CURRENT_EMOJI_LOCALIZER != null;
|
||||
}
|
||||
|
||||
/** @testexport */
|
||||
export function _resetEmojiLocalizer(): void {
|
||||
CURRENT_EMOJI_LOCALIZER = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* API
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type Emoji = string & { Emoji: never };
|
||||
|
||||
export namespace Emoji {
|
||||
export type Parent = Emoji & { EmojiParent: never };
|
||||
export type Variant = Emoji & { EmojiVariant: never };
|
||||
export type SkinToneVariant = Variant & { EmojiSkinToneVariant: never };
|
||||
|
||||
export type ShortName = string & { EmojiShortName: never };
|
||||
export type CompletionLabel = string & { EmojiCompletionLabel: never };
|
||||
export type CompletionQuery = string & { EmojiCompletionQuery: never };
|
||||
|
||||
export enum SkinTone {
|
||||
None = '',
|
||||
Type1 = '1F3FB',
|
||||
Type2 = '1F3FC',
|
||||
Type3 = '1F3FD',
|
||||
Type4 = '1F3FE',
|
||||
Type5 = '1F3FF',
|
||||
}
|
||||
|
||||
export enum Category {
|
||||
SMILIES_AND_PEOPLE = 'SMILIES_AND_PEOPLE',
|
||||
ANIMALS_AND_NATURE = 'ANIMALS_AND_NATURE',
|
||||
FOOD_AND_DRINK = 'FOOD_AND_DRINK',
|
||||
TRAVEL_AND_PLACES = 'TRAVEL_AND_PLACES',
|
||||
ACTIVITIES = 'ACTIVITIES',
|
||||
OBJECTS = 'OBJECTS',
|
||||
SYMBOLS = 'SYMBOLS',
|
||||
FLAGS = 'FLAGS',
|
||||
}
|
||||
|
||||
export function getDebugLabel(input: string): string {
|
||||
return Array.from(input.slice(0, 12), char => {
|
||||
const num = char.codePointAt(0) ?? 0;
|
||||
const hex = num.toString(16).toUpperCase().padStart(4, '0');
|
||||
return `U+${hex}`;
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
export function getDisplayLabel(emoji: Emoji): ShortName {
|
||||
return getLocalizer().getShortName(getParent(emoji));
|
||||
}
|
||||
|
||||
export function getCompletionLabel(emoji: Emoji): CompletionLabel {
|
||||
return _toEmojiCompletionLabel(getDisplayLabel(emoji));
|
||||
}
|
||||
|
||||
export function isEmoji(input: string): input is Emoji {
|
||||
return isParent(input) || isSkinToneVariant(input);
|
||||
}
|
||||
|
||||
export function isParent(emoji: string): emoji is Parent {
|
||||
return Object.hasOwn(EMOJI_DATA.emojis, emoji);
|
||||
}
|
||||
|
||||
export function isSkinToneVariant(emoji: string): emoji is SkinToneVariant {
|
||||
return Object.hasOwn(EMOJI_DATA.parents, emoji);
|
||||
}
|
||||
|
||||
export function matchShortName(shortName: string): Parent | null {
|
||||
const query = _toEmojiCompletionQuery(shortName as ShortName);
|
||||
return getLocalizer().matchCompletion(query);
|
||||
}
|
||||
|
||||
export function getCategoryParents(
|
||||
category: Category
|
||||
): ReadonlyArray<Parent> {
|
||||
return EMOJI_DATA.categories[category];
|
||||
}
|
||||
|
||||
export function getParent(emoji: Emoji): Parent {
|
||||
if (isSkinToneVariant(emoji)) {
|
||||
return assert(EMOJI_DATA.parents[emoji], `Missing parent for "${emoji}"`);
|
||||
}
|
||||
assert(isParent(emoji), `Expected "${emoji}" to be a parent`);
|
||||
return emoji;
|
||||
}
|
||||
|
||||
export function hasSkinToneVariants(emoji: Parent): boolean {
|
||||
return getParentInfo(emoji).skinTones != null;
|
||||
}
|
||||
|
||||
export function getDefaultVariant(parent: Parent): Variant {
|
||||
return getVariant(parent, SkinTone.None);
|
||||
}
|
||||
|
||||
export function getVariant(parent: Parent, skinTone: SkinTone): Variant {
|
||||
if (skinTone === SkinTone.None) {
|
||||
return parent as Emoji as Variant;
|
||||
}
|
||||
const { skinTones } = getParentInfo(parent);
|
||||
if (skinTones == null) {
|
||||
return parent as Emoji as Variant;
|
||||
}
|
||||
return assert(skinTones[skinTone]);
|
||||
}
|
||||
|
||||
export function unsafeCastMaybeInvalidStringToVariant<T extends string>(
|
||||
emoji: T extends Emoji ? never : T
|
||||
): Variant {
|
||||
return emoji as string as Variant;
|
||||
}
|
||||
|
||||
export function ignorePreferredSkinTone(emoji: Emoji): Variant {
|
||||
return emoji as Variant;
|
||||
}
|
||||
|
||||
export const BAR_CHART = PARENT_AND_ONLY_VARIANT('📊');
|
||||
export const BEE = PARENT_AND_ONLY_VARIANT('🐝');
|
||||
export const BELL = PARENT_AND_ONLY_VARIANT('🔔');
|
||||
export const BIKE = PARENT_AND_ONLY_VARIANT('🚲');
|
||||
export const BLACK_CIRCLE = PARENT_AND_ONLY_VARIANT('⚫');
|
||||
export const BLUE_HEART = PARENT_AND_ONLY_VARIANT('💙');
|
||||
export const BLUSH = PARENT_AND_ONLY_VARIANT('😊');
|
||||
export const BULB = PARENT_AND_ONLY_VARIANT('💡');
|
||||
export const BUST_IN_SILHOUETTE = PARENT_AND_ONLY_VARIANT('👤');
|
||||
export const CAKE = PARENT_AND_ONLY_VARIANT('🍰');
|
||||
export const CALENDAR = PARENT_AND_ONLY_VARIANT('📆');
|
||||
export const CALL_ME_HAND = PARENT('🤙');
|
||||
export const CAMERA = PARENT_AND_ONLY_VARIANT('📷');
|
||||
export const CAR = PARENT_AND_ONLY_VARIANT('🚗');
|
||||
export const CAT = PARENT_AND_ONLY_VARIANT('🐱');
|
||||
export const CHECKMARK = PARENT_AND_ONLY_VARIANT('✅');
|
||||
export const CLINKING_GLASSES = PARENT_AND_ONLY_VARIANT('🥂');
|
||||
export const COFFEE = PARENT_AND_ONLY_VARIANT('☕');
|
||||
export const CONFETTI_BALL = PARENT_AND_ONLY_VARIANT('🎊');
|
||||
export const CONFUSED = PARENT_AND_ONLY_VARIANT('😕');
|
||||
export const COOL = PARENT_AND_ONLY_VARIANT('🆒');
|
||||
export const CREDIT_CARD = PARENT_AND_ONLY_VARIANT('💳');
|
||||
export const CRY = PARENT_AND_ONLY_VARIANT('😢');
|
||||
export const DOG = PARENT_AND_ONLY_VARIANT('🐶');
|
||||
export const DOTTED_LINE_FACE = PARENT_AND_ONLY_VARIANT('🫥');
|
||||
export const ELEPHANT = PARENT_AND_ONLY_VARIANT('🐘');
|
||||
export const EXPLODING_HEAD = PARENT_AND_ONLY_VARIANT('🤯');
|
||||
export const FACE_WITH_SPIRAL_EYES = PARENT_AND_ONLY_VARIANT('😵💫');
|
||||
export const FERRIS_WHEEL = PARENT_AND_ONLY_VARIANT('🎡');
|
||||
export const FIRE = PARENT_AND_ONLY_VARIANT('🔥');
|
||||
export const FIREWORK_SPARKLER = PARENT_AND_ONLY_VARIANT('🎇');
|
||||
export const FOLDED_HANDS = PARENT('🙏');
|
||||
export const FRIED_SHRIMP = PARENT_AND_ONLY_VARIANT('🍤');
|
||||
export const GHOST = PARENT_AND_ONLY_VARIANT('👻');
|
||||
export const GREEN_CIRCLE = PARENT_AND_ONLY_VARIANT('🟢');
|
||||
export const GRIMACING = PARENT_AND_ONLY_VARIANT('😬');
|
||||
export const GRINNING = PARENT_AND_ONLY_VARIANT('😀');
|
||||
export const HAND = PARENT('✋');
|
||||
export const HEART = PARENT_AND_ONLY_VARIANT('❤️');
|
||||
export const HEART_ON_FIRE = PARENT_AND_ONLY_VARIANT('❤️🔥');
|
||||
export const HOUSE = PARENT_AND_ONLY_VARIANT('🏠');
|
||||
export const JOY = PARENT_AND_ONLY_VARIANT('😂');
|
||||
export const KISSING_HEART = PARENT_AND_ONLY_VARIANT('😘');
|
||||
export const LIPSTICK = PARENT_AND_ONLY_VARIANT('💄');
|
||||
export const MICROPHONE = PARENT_AND_ONLY_VARIANT('🎤');
|
||||
export const MOBILE_PHONE_OFF = PARENT_AND_ONLY_VARIANT('📴');
|
||||
export const MONKEY = PARENT_AND_ONLY_VARIANT('🐒');
|
||||
export const MOVIE_CAMERA = PARENT_AND_ONLY_VARIANT('🎥');
|
||||
export const MUSCLE = PARENT('💪');
|
||||
export const NAIL_CARE = PARENT('💅');
|
||||
export const NEUTRAL_FACE = PARENT_AND_ONLY_VARIANT('😐');
|
||||
export const NO_BELL = PARENT_AND_ONLY_VARIANT('🔕');
|
||||
export const OK_HAND = PARENT('👌');
|
||||
export const ONE = PARENT_AND_ONLY_VARIANT('1️⃣');
|
||||
export const ONE_HUNDRED = PARENT_AND_ONLY_VARIANT('💯');
|
||||
export const OPEN_MOUTH = PARENT_AND_ONLY_VARIANT('😮');
|
||||
export const PAPERCLIP = PARENT_AND_ONLY_VARIANT('📎');
|
||||
export const PARKING = PARENT_AND_ONLY_VARIANT('🅿️');
|
||||
export const PLUS = PARENT_AND_ONLY_VARIANT('➕');
|
||||
export const POULTRY_LEG = PARENT_AND_ONLY_VARIANT('🍗');
|
||||
export const PUSHPIN = PARENT_AND_ONLY_VARIANT('📌');
|
||||
export const RAGE = PARENT_AND_ONLY_VARIANT('😡');
|
||||
export const REPEAT = PARENT_AND_ONLY_VARIANT('🔁');
|
||||
export const ROCKET = PARENT_AND_ONLY_VARIANT('🚀');
|
||||
export const ROSE = PARENT_AND_ONLY_VARIANT('🌹');
|
||||
export const SHARK = PARENT_AND_ONLY_VARIANT('🦈');
|
||||
export const SHRUG = PARENT('🤷');
|
||||
export const SHUSHING_FACE = PARENT_AND_ONLY_VARIANT('🤫');
|
||||
export const SKULL = PARENT_AND_ONLY_VARIANT('💀');
|
||||
export const SLEEPING = PARENT_AND_ONLY_VARIANT('😴');
|
||||
export const SLIGHTLY_FROWNING_FACE = PARENT_AND_ONLY_VARIANT('🙁');
|
||||
export const SLIGHTLY_SMILING_FACE = PARENT_AND_ONLY_VARIANT('🙂');
|
||||
export const SMILE = PARENT_AND_ONLY_VARIANT('😄');
|
||||
export const SMILE_CAT = PARENT_AND_ONLY_VARIANT('😸');
|
||||
export const SOB = PARENT_AND_ONLY_VARIANT('😭');
|
||||
export const SPARKLE = PARENT_AND_ONLY_VARIANT('❇️');
|
||||
export const SPARKLES = PARENT_AND_ONLY_VARIANT('✨');
|
||||
export const SPARKLING_HEART = PARENT_AND_ONLY_VARIANT('💖');
|
||||
export const SPEAKER = PARENT_AND_ONLY_VARIANT('🔈');
|
||||
export const SPEECH_BALLOON = PARENT_AND_ONLY_VARIANT('💬');
|
||||
export const STAR = PARENT_AND_ONLY_VARIANT('⭐');
|
||||
export const STUCK_OUT_TONGUE = PARENT_AND_ONLY_VARIANT('😛');
|
||||
export const SUNGLASSES = PARENT_AND_ONLY_VARIANT('😎');
|
||||
export const SUNNY = PARENT_AND_ONLY_VARIANT('☀️');
|
||||
export const SWEAT_SMILE = PARENT_AND_ONLY_VARIANT('😅');
|
||||
export const TADA = PARENT_AND_ONLY_VARIANT('🎉');
|
||||
export const THE_HORNS = PARENT('🤘');
|
||||
export const THREE = PARENT_AND_ONLY_VARIANT('3️⃣');
|
||||
export const THUMBS_DOWN = PARENT('👎');
|
||||
export const THUMBS_UP = PARENT('👍');
|
||||
export const TOPHAT = PARENT_AND_ONLY_VARIANT('🎩');
|
||||
export const TURTLE = PARENT_AND_ONLY_VARIANT('🐢');
|
||||
export const TWO = PARENT_AND_ONLY_VARIANT('2️⃣');
|
||||
export const WARNING = PARENT_AND_ONLY_VARIANT('⚠️');
|
||||
export const WAVE = PARENT('👋');
|
||||
export const WEIGHT_LIFTER = PARENT('🏋️');
|
||||
export const WHITE_HEART = PARENT_AND_ONLY_VARIANT('🤍');
|
||||
export const WILTED_FLOWER = PARENT_AND_ONLY_VARIANT('🥀');
|
||||
export const WINK = PARENT_AND_ONLY_VARIANT('😉');
|
||||
export const ZIPPER_MOUTH_FACE = PARENT_AND_ONLY_VARIANT('🤐');
|
||||
export const ZZZ = PARENT_AND_ONLY_VARIANT('💤');
|
||||
|
||||
const DEFAULT_PREFERRED_REACTION_EMOJI: Array<Parent> = [
|
||||
HEART,
|
||||
THUMBS_UP,
|
||||
THUMBS_DOWN,
|
||||
JOY,
|
||||
OPEN_MOUTH,
|
||||
CRY,
|
||||
];
|
||||
|
||||
export function getDefaultPreferredReactionEmojis(
|
||||
skinTone: SkinTone
|
||||
): ReadonlyArray<Variant> {
|
||||
return DEFAULT_PREFERRED_REACTION_EMOJI.map(emoji => {
|
||||
return getVariant(emoji, skinTone);
|
||||
});
|
||||
}
|
||||
|
||||
export const CATEGORY_ORDER: ReadonlyArray<Category> = [
|
||||
Category.SMILIES_AND_PEOPLE,
|
||||
Category.ANIMALS_AND_NATURE,
|
||||
Category.FOOD_AND_DRINK,
|
||||
Category.TRAVEL_AND_PLACES,
|
||||
Category.ACTIVITIES,
|
||||
Category.OBJECTS,
|
||||
Category.SYMBOLS,
|
||||
Category.FLAGS,
|
||||
];
|
||||
|
||||
export const SKIN_TONE_ORDER: ReadonlyArray<SkinTone> = [
|
||||
SkinTone.None,
|
||||
SkinTone.Type1,
|
||||
SkinTone.Type2,
|
||||
SkinTone.Type3,
|
||||
SkinTone.Type4,
|
||||
SkinTone.Type5,
|
||||
];
|
||||
|
||||
export function* iterateAllVariants(): Generator<Variant, void, void> {
|
||||
for (const category of Object.values(EMOJI_DATA.categories)) {
|
||||
for (const parent of category) {
|
||||
yield getDefaultVariant(parent);
|
||||
const info = getParentInfo(parent);
|
||||
if (info.skinTones != null) {
|
||||
for (const skinTone of SKIN_TONE_ORDER) {
|
||||
if (skinTone === SkinTone.None) {
|
||||
continue;
|
||||
}
|
||||
yield info.skinTones[skinTone];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
function isEmptyString(input: string): boolean {
|
||||
return input === '' || input.trim() === '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Emoji.replaceEmojiWithSpaces()
|
||||
* ------------------------------
|
||||
*/
|
||||
|
||||
const SPACES_CACHE: Record<number, string> = {};
|
||||
const MAX_EMOJI_UTF16_LENGTH = 15;
|
||||
|
||||
for (let i = 0; i < MAX_EMOJI_UTF16_LENGTH; i += 1) {
|
||||
SPACES_CACHE[i] = ' '.repeat(i);
|
||||
}
|
||||
|
||||
export function replaceEmojiWithSpaces(input: string): string {
|
||||
if (isEmptyString(input)) {
|
||||
return input; // fast path
|
||||
}
|
||||
const emojiRegex = getEmojiRegex();
|
||||
return input.replaceAll(emojiRegex, match => {
|
||||
const length = match.length;
|
||||
return SPACES_CACHE[length] ?? ' '.repeat(length);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Emoji.stripEmojiFromText()
|
||||
* --------------------------
|
||||
*/
|
||||
|
||||
export function stripEmojiFromText(input: string): string {
|
||||
if (isEmptyString(input)) {
|
||||
return input; // fast path
|
||||
}
|
||||
const emojiRegex = getEmojiRegex();
|
||||
return input.replaceAll(emojiRegex, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Emoji.getMatches()
|
||||
* ------------------
|
||||
*/
|
||||
|
||||
export type Match = Readonly<{
|
||||
emoji: Variant;
|
||||
offset: number;
|
||||
}>;
|
||||
|
||||
const MAX_EMOJI_TO_MATCH = 5000;
|
||||
|
||||
export function* getMatches(input: string): Generator<Match, void, void> {
|
||||
if (isEmptyString(input)) {
|
||||
return; // fast path
|
||||
}
|
||||
|
||||
const emojiRegex = getEmojiRegex();
|
||||
let count = 0;
|
||||
for (const match of input.matchAll(emojiRegex)) {
|
||||
const emoji = match[0];
|
||||
if (isEmoji(emoji)) {
|
||||
yield {
|
||||
emoji: ignorePreferredSkinTone(emoji),
|
||||
offset: match.index,
|
||||
};
|
||||
}
|
||||
count += 1;
|
||||
if (count >= MAX_EMOJI_TO_MATCH) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emoji.getSegments()
|
||||
* -------------------
|
||||
*/
|
||||
|
||||
export type TextSegment = Readonly<{
|
||||
kind: 'text';
|
||||
value: string;
|
||||
offset: number;
|
||||
}>;
|
||||
|
||||
export type EmojiSegment = Readonly<{
|
||||
kind: 'emoji';
|
||||
value: Variant;
|
||||
offset: number;
|
||||
}>;
|
||||
|
||||
export type Segment = TextSegment | EmojiSegment;
|
||||
|
||||
export function* getSegments(input: string): Generator<Segment, void, void> {
|
||||
if (isEmptyString(input)) {
|
||||
yield { kind: 'text', value: input, offset: 0 }; // fast path
|
||||
return;
|
||||
}
|
||||
|
||||
let cursor = 0;
|
||||
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-qualifier allow this call to be spied on
|
||||
for (const match of Emoji.getMatches(input)) {
|
||||
const { offset, emoji } = match;
|
||||
|
||||
if (offset > cursor) {
|
||||
const value = input.slice(cursor, offset);
|
||||
yield { kind: 'text', value, offset: cursor };
|
||||
}
|
||||
|
||||
yield { kind: 'emoji', value: emoji, offset };
|
||||
cursor = offset + emoji.length;
|
||||
}
|
||||
|
||||
if (cursor < input.length) {
|
||||
yield { kind: 'text', value: input.slice(cursor), offset: cursor };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* getEmojiOnlyCount()
|
||||
* -------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Count the number of emoji in some text, returns 0 if there is non-emoji
|
||||
* text, or there is too many emoji
|
||||
* @internal
|
||||
*/
|
||||
function getEmojiOnlyCount(input: string, limit: number): number {
|
||||
if (isEmptyString(input)) {
|
||||
return 0; // fast path
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-qualifier allow this call to be spied on
|
||||
for (const segment of Emoji.getSegments(input)) {
|
||||
if (segment.kind === 'text') {
|
||||
return 0; // found other text
|
||||
}
|
||||
if (segment.kind === 'emoji') {
|
||||
count += 1;
|
||||
}
|
||||
if (count > limit) {
|
||||
return 0; // too many
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emoji.isLoneEmoji()
|
||||
* -------------------
|
||||
*/
|
||||
|
||||
export function isLoneEmoji(input: string): boolean {
|
||||
const count = getEmojiOnlyCount(input, 1);
|
||||
return count === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emoji.getJumboEmojiCount()
|
||||
* --------------------------
|
||||
*/
|
||||
|
||||
export type JumboEmojiCount = null | 1 | 2 | 3 | 4 | 5;
|
||||
|
||||
export const MAX_JUMBO_EMOJI_COUNT = 5 satisfies JumboEmojiCount;
|
||||
|
||||
export function getJumboEmojiCount(input: string): JumboEmojiCount {
|
||||
const count = getEmojiOnlyCount(input, MAX_JUMBO_EMOJI_COUNT);
|
||||
if (count === 0) {
|
||||
return null;
|
||||
}
|
||||
assert(count <= MAX_JUMBO_EMOJI_COUNT);
|
||||
return count as JumboEmojiCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Localization
|
||||
*/
|
||||
|
||||
export type EmojiLocaleItem = Readonly<{
|
||||
emoji: string;
|
||||
tags: ReadonlyArray<string>;
|
||||
rank: number;
|
||||
}>;
|
||||
|
||||
export type EmojiLocaleData = ReadonlyArray<EmojiLocaleItem>;
|
||||
|
||||
export function setupLocale(localeData: EmojiLocaleData): void {
|
||||
const LOCALE_INDEXES = lazy(() => {
|
||||
const localeItemsByParent = new Map<Parent, EmojiLocaleItem>();
|
||||
for (const item of localeData) {
|
||||
if (isParent(item.emoji)) {
|
||||
localeItemsByParent.set(item.emoji, item);
|
||||
}
|
||||
}
|
||||
|
||||
const parentToShortName = new Map<Parent, ShortName>();
|
||||
const completionToParent = new Map<CompletionQuery, Parent>();
|
||||
|
||||
const searchIndex: Array<SearchIndexEntry> = [];
|
||||
|
||||
for (const [parent, defaultRank] of ALL_PARENTS_WITH_DEFAULT_RANK()) {
|
||||
const info = getParentInfo(parent);
|
||||
const item = localeItemsByParent.get(parent);
|
||||
|
||||
let allShortNames = (item?.tags ?? []) as Array<ShortName>;
|
||||
allShortNames.push(info.shortName);
|
||||
if (info.shortNameAlts != null) {
|
||||
allShortNames = allShortNames.concat(info.shortNameAlts);
|
||||
}
|
||||
|
||||
const shortName = assert(allShortNames.at(0));
|
||||
const shortNameAlts = allShortNames.slice(1);
|
||||
|
||||
parentToShortName.set(parent, shortName);
|
||||
|
||||
const completion = _toEmojiCompletionQuery(shortName);
|
||||
const completionAlts = shortNameAlts.map(shortNameAlt => {
|
||||
return _toEmojiCompletionQuery(shortNameAlt);
|
||||
});
|
||||
|
||||
completionToParent.set(completion, parent);
|
||||
|
||||
searchIndex.push({
|
||||
parent,
|
||||
rank: item?.rank ?? defaultRank,
|
||||
completion,
|
||||
completionAlts,
|
||||
emoticon: info.emoticon ?? null,
|
||||
emoticonAlts: [],
|
||||
});
|
||||
}
|
||||
|
||||
return { parentToShortName, completionToParent, searchIndex };
|
||||
});
|
||||
|
||||
CURRENT_EMOJI_LOCALIZER = {
|
||||
getShortName(parent) {
|
||||
return (
|
||||
LOCALE_INDEXES().parentToShortName.get(parent) ??
|
||||
DEFAULT_ENGLISH_LOCALIZER.getShortName(parent)
|
||||
);
|
||||
},
|
||||
matchCompletion(completion) {
|
||||
return (
|
||||
LOCALE_INDEXES().completionToParent.get(completion) ??
|
||||
DEFAULT_ENGLISH_LOCALIZER.matchCompletion(completion)
|
||||
);
|
||||
},
|
||||
getSearchFuses() {
|
||||
return createEmojiSearchFuses(LOCALE_INDEXES().searchIndex);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function search(input: string, limit = 200): ReadonlyArray<Parent> {
|
||||
if (isEmptyString(input)) {
|
||||
return []; // fast path
|
||||
}
|
||||
|
||||
const fuses = getLocalizer().getSearchFuses();
|
||||
|
||||
const query = _toEmojiCompletionQuery(input.trim() as ShortName);
|
||||
const fuse = query.length < 2 ? fuses.getExact() : fuses.getFuzzy();
|
||||
const results = fuse.search(query.substring(0, 32));
|
||||
|
||||
const scores = results.map(result => {
|
||||
const { item, matches } = result;
|
||||
const { parent } = item;
|
||||
|
||||
const match = matches?.[0]?.value ?? item.completion;
|
||||
|
||||
let score: number;
|
||||
if (match.startsWith(query)) {
|
||||
// Exact prefix matches in [0,1] range
|
||||
score = 1 - query.length / match.length;
|
||||
} else {
|
||||
const queryScore = result.score ?? 0;
|
||||
const rankScore = item.rank / fuses.size;
|
||||
// Other matches in [1,] range ordered by score and rank
|
||||
score = 1 + queryScore + rankScore;
|
||||
}
|
||||
|
||||
return { parent, score };
|
||||
});
|
||||
|
||||
const sorted = scores.toSorted((a, b) => {
|
||||
return a.score - b.score;
|
||||
});
|
||||
|
||||
return sorted.slice(0, limit).map(result => {
|
||||
return result.parent;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -70,7 +70,6 @@ import { updateIdentityKey } from './services/profiles.preload.ts';
|
||||
import { initializeUpdateListener } from './services/updateListener.preload.ts';
|
||||
import { RoutineProfileRefresher } from './routineProfileRefresh.preload.ts';
|
||||
import { isOlderThan } from './util/timestamp.std.ts';
|
||||
import { isValidReactionEmoji } from './reactions/isValidReactionEmoji.std.ts';
|
||||
import { safeParsePartial } from './util/schemas.std.ts';
|
||||
import { PollVoteSchema, PollTerminateSchema } from './types/Polls.dom.ts';
|
||||
import type { ConversationModel } from './models/conversations.preload.ts';
|
||||
@ -295,6 +294,7 @@ import { initMessageCleanup } from './services/messageStateCleanup.dom.ts';
|
||||
import { MessageCache } from './services/MessageCache.preload.ts';
|
||||
import { saveAndNotify } from './messages/saveAndNotify.preload.ts';
|
||||
import { getBackupKeyHash } from './services/backups/crypto.preload.ts';
|
||||
import { Emoji } from './axo/emoji.std.ts';
|
||||
|
||||
const { isNumber, throttle } = lodash;
|
||||
|
||||
@ -2547,7 +2547,13 @@ async function startApp(): Promise<void> {
|
||||
|
||||
const { reaction, timestamp } = data.message;
|
||||
|
||||
if (!isValidReactionEmoji(reaction.emoji)) {
|
||||
if (reaction.emoji == null) {
|
||||
log.warn('Received a reaction without an emoji. Dropping it');
|
||||
confirm();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Emoji.isEmoji(reaction.emoji)) {
|
||||
log.warn('Received an invalid reaction emoji. Dropping it');
|
||||
confirm();
|
||||
return;
|
||||
@ -3103,7 +3109,13 @@ async function startApp(): Promise<void> {
|
||||
'Reaction without targetAuthorAci'
|
||||
);
|
||||
|
||||
if (!isValidReactionEmoji(reaction.emoji)) {
|
||||
if (reaction.emoji == null) {
|
||||
log.warn('Received a reaction without an emoji. Dropping it');
|
||||
confirm();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Emoji.isEmoji(reaction.emoji)) {
|
||||
log.warn('Received an invalid reaction emoji. Dropping it');
|
||||
confirm();
|
||||
return;
|
||||
|
||||
@ -8,6 +8,7 @@ import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './AnimatedEmojiGalore.dom.tsx';
|
||||
import { AnimatedEmojiGalore } from './AnimatedEmojiGalore.dom.tsx';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
export default {
|
||||
title: 'Components/AnimatedEmojiGalore',
|
||||
@ -15,7 +16,7 @@ export default {
|
||||
|
||||
function getDefaultProps(): PropsType {
|
||||
return {
|
||||
emoji: '❤️',
|
||||
emoji: Emoji.HEART,
|
||||
onAnimationEnd: action('onAnimationEnd'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -8,16 +8,12 @@ import lodash from 'lodash';
|
||||
import { useReducedMotion } from '../hooks/useReducedMotion.dom.ts';
|
||||
import { FunStaticEmoji } from './fun/FunEmoji.dom.tsx';
|
||||
import { strictAssert } from '../util/assert.std.ts';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from './fun/data/emojis.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { random } = lodash;
|
||||
|
||||
export type PropsType = {
|
||||
emoji: string;
|
||||
emoji: Emoji.Variant;
|
||||
onAnimationEnd: () => unknown;
|
||||
rotate?: number;
|
||||
scale?: number;
|
||||
@ -48,9 +44,7 @@ export function AnimatedEmojiGalore({
|
||||
emoji,
|
||||
onAnimationEnd,
|
||||
}: PropsType): JSX.Element {
|
||||
strictAssert(isEmojiVariantValue(emoji), 'Must be valid english short name');
|
||||
const emojiVariantKey = getEmojiVariantKeyByValue(emoji);
|
||||
const emojiVariant = getEmojiVariantByKey(emojiVariantKey);
|
||||
strictAssert(Emoji.isEmoji(emoji), 'Must be valid emoji variant value');
|
||||
|
||||
const reducedMotion = useReducedMotion();
|
||||
const [springs] = useSprings(NUM_EMOJIS, i => ({
|
||||
@ -80,7 +74,7 @@ export function AnimatedEmojiGalore({
|
||||
),
|
||||
}}
|
||||
>
|
||||
<FunStaticEmoji size={48} emoji={emojiVariant} role="presentation" />
|
||||
<FunStaticEmoji size={48} emoji={emoji} role="presentation" />
|
||||
</animated.div>
|
||||
))}
|
||||
</>
|
||||
|
||||
@ -18,11 +18,12 @@ import { createPortal } from 'react-dom';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useIsMounted } from '../hooks/useIsMounted.std.ts';
|
||||
import { CallReactionBurstEmoji } from './CallReactionBurstEmoji.dom.tsx';
|
||||
import type { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const LIFETIME = 3000;
|
||||
|
||||
export type CallReactionBurstType = {
|
||||
values: Array<string>;
|
||||
values: Array<Emoji>;
|
||||
};
|
||||
|
||||
type CallReactionBurstStateType = CallReactionBurstType & {
|
||||
|
||||
@ -14,11 +14,12 @@ import lodash from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Emojify } from './conversation/Emojify.dom.tsx';
|
||||
import { useReducedMotion } from '../hooks/useReducedMotion.dom.ts';
|
||||
import type { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { random } = lodash;
|
||||
|
||||
export type PropsType = {
|
||||
values: Array<string>;
|
||||
values: Array<Emoji>;
|
||||
onAnimationEnd?: () => unknown;
|
||||
};
|
||||
|
||||
@ -109,7 +110,7 @@ export function CallReactionBurstEmoji({ values }: PropsType): JSX.Element {
|
||||
}
|
||||
|
||||
type AnimatedEmojiProps = {
|
||||
value: string;
|
||||
value: Emoji;
|
||||
fromRotate: number;
|
||||
fromX: number;
|
||||
fromY: number;
|
||||
|
||||
@ -24,7 +24,6 @@ import type { ConversationType } from '../state/ducks/conversations.preload.ts';
|
||||
import { AvatarColors } from '../types/Colors.std.ts';
|
||||
import type { PropsType } from './CallScreen.dom.tsx';
|
||||
import { CallScreen as UnwrappedCallScreen } from './CallScreen.dom.tsx';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../reactions/constants.std.ts';
|
||||
import { missingCaseError } from '../util/missingCaseError.std.ts';
|
||||
import {
|
||||
getAvatarPath,
|
||||
@ -38,6 +37,7 @@ import { MINUTE } from '../util/durations/index.std.ts';
|
||||
import { strictAssert } from '../util/assert.std.ts';
|
||||
import { generateAci } from '../test-helpers/serviceIdUtils.std.ts';
|
||||
import { renderCallingParticipantMenu } from './CallingParticipantMenu.dom.stories.tsx';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { sample, shuffle, times } = lodash;
|
||||
|
||||
@ -834,7 +834,18 @@ export function GroupCallReactionsSkinTones(): JSX.Element {
|
||||
const activeCall = useReactionsEmitter({
|
||||
activeCall: props.activeCall as ActiveGroupCallType,
|
||||
frequency: 500,
|
||||
emojis: ['👍', '👍🏻', '👍🏼', '👍🏽', '👍🏾', '👍🏿', '❤️', '😂', '😮', '😢'],
|
||||
emojis: [
|
||||
Emoji.getVariant(Emoji.THUMBS_UP, Emoji.SkinTone.None),
|
||||
Emoji.getVariant(Emoji.THUMBS_UP, Emoji.SkinTone.Type1),
|
||||
Emoji.getVariant(Emoji.THUMBS_UP, Emoji.SkinTone.Type2),
|
||||
Emoji.getVariant(Emoji.THUMBS_UP, Emoji.SkinTone.Type3),
|
||||
Emoji.getVariant(Emoji.THUMBS_UP, Emoji.SkinTone.Type4),
|
||||
Emoji.getVariant(Emoji.THUMBS_UP, Emoji.SkinTone.Type5),
|
||||
Emoji.HEART,
|
||||
Emoji.JOY,
|
||||
Emoji.OPEN_MOUTH,
|
||||
Emoji.CRY,
|
||||
],
|
||||
});
|
||||
|
||||
return <CallScreen {...props} activeCall={activeCall} />;
|
||||
@ -845,10 +856,10 @@ export function GroupCallReactionsManyInOrder(): JSX.Element {
|
||||
const remoteParticipants = allRemoteParticipants.slice(0, 5);
|
||||
const reactions = remoteParticipants.map((participant, i) => {
|
||||
const { demuxId } = participant;
|
||||
const value =
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI[
|
||||
i % DEFAULT_PREFERRED_REACTION_EMOJI.length
|
||||
];
|
||||
const defaults = Emoji.getDefaultPreferredReactionEmojis(
|
||||
Emoji.SkinTone.None
|
||||
);
|
||||
const value = defaults[i % defaults.length];
|
||||
strictAssert(value, 'Missing value');
|
||||
return { timestamp, demuxId, value };
|
||||
});
|
||||
@ -868,12 +879,12 @@ function useReactionsEmitter({
|
||||
activeCall,
|
||||
frequency = 2000,
|
||||
removeAfter = 5000,
|
||||
emojis = DEFAULT_PREFERRED_REACTION_EMOJI,
|
||||
emojis = Emoji.getDefaultPreferredReactionEmojis(Emoji.SkinTone.None),
|
||||
}: {
|
||||
activeCall: ActiveGroupCallType;
|
||||
frequency?: number;
|
||||
removeAfter?: number;
|
||||
emojis?: Array<string>;
|
||||
emojis?: ReadonlyArray<Emoji.Variant>;
|
||||
}) {
|
||||
const [call, setCall] = useState(activeCall);
|
||||
useEffect(() => {
|
||||
|
||||
@ -103,15 +103,6 @@ import { assertDev } from '../util/assert.std.ts';
|
||||
import { CallingPendingParticipants } from './CallingPendingParticipants.dom.tsx';
|
||||
import type { CallingImageDataCache } from './CallManager.dom.tsx';
|
||||
import { FunStaticEmoji } from './fun/FunEmoji.dom.tsx';
|
||||
import {
|
||||
getEmojiDebugLabel,
|
||||
getEmojiParentByKey,
|
||||
getEmojiParentKeyByVariantKey,
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from './fun/data/emojis.std.ts';
|
||||
import { useFunEmojiLocalizer } from './fun/useFunEmojiLocalizer.dom.tsx';
|
||||
import {
|
||||
BeforeNavigateResponse,
|
||||
beforeNavigateService,
|
||||
@ -124,6 +115,7 @@ import {
|
||||
PIP_MINIMUM_LOCAL_VIDEO_HEIGHT_MULTIPLIER,
|
||||
} from './CallingPip.dom.tsx';
|
||||
import type { PropsType as SmartCallingParticipantMenuProps } from '../state/smart/CallingParticipantMenu.preload.tsx';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { isEqual, noop } = lodash;
|
||||
|
||||
@ -1398,17 +1390,16 @@ function useReactionsToast(props: UseReactionsToastType): void {
|
||||
Map<
|
||||
string,
|
||||
{
|
||||
value: string;
|
||||
originalValue: string;
|
||||
value: Emoji;
|
||||
originalValue: Emoji;
|
||||
isBursted: boolean;
|
||||
expireAt: number;
|
||||
demuxId: number;
|
||||
}
|
||||
>
|
||||
>(new Map());
|
||||
const burstsShown = useRef<Map<string, number>>(new Map());
|
||||
const burstsShown = useRef<Map<Emoji | Emoji.Variant, number>>(new Map());
|
||||
const { showToast } = useCallingToasts();
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
|
||||
useEffect(() => {
|
||||
setPreviousReactions(reactions);
|
||||
@ -1429,16 +1420,13 @@ function useReactionsToast(props: UseReactionsToastType): void {
|
||||
|
||||
const key = `reactions-${timestamp}-${demuxId}`;
|
||||
|
||||
if (!isEmojiVariantValue(value)) {
|
||||
if (!Emoji.isEmoji(value)) {
|
||||
log.error(
|
||||
`Expected a valid emoji value, got ${getEmojiDebugLabel(value)}`
|
||||
`Expected a valid emoji value, got ${Emoji.getDebugLabel(value)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const emojiVariantKey = getEmojiVariantKeyByValue(value);
|
||||
const emojiVariant = getEmojiVariantByKey(emojiVariantKey);
|
||||
|
||||
showToast({
|
||||
key,
|
||||
onlyShowOnce: true,
|
||||
@ -1447,9 +1435,9 @@ function useReactionsToast(props: UseReactionsToastType): void {
|
||||
<span className="CallingReactionsToasts__reaction">
|
||||
<FunStaticEmoji
|
||||
role="img"
|
||||
aria-label={emojiLocalizer.getLocaleShortName(emojiVariantKey)}
|
||||
aria-label={Emoji.getDisplayLabel(value)}
|
||||
size={28}
|
||||
emoji={emojiVariant}
|
||||
emoji={Emoji.ignorePreferredSkinTone(value)}
|
||||
/>
|
||||
{demuxId === localDemuxId ||
|
||||
(ourServiceId && conversation?.serviceId === ourServiceId)
|
||||
@ -1472,11 +1460,9 @@ function useReactionsToast(props: UseReactionsToastType): void {
|
||||
);
|
||||
// Normalize skin tone emoji to calculate burst threshold, but save original
|
||||
// value to show in the burst animation
|
||||
const emojiParentKey = getEmojiParentKeyByVariantKey(emojiVariantKey);
|
||||
const emojiParent = getEmojiParentByKey(emojiParentKey);
|
||||
const normalizedValue = emojiParent.value;
|
||||
const emojiParent = Emoji.getParent(value);
|
||||
reactionsShown.current.set(key, {
|
||||
value: normalizedValue,
|
||||
value: emojiParent,
|
||||
originalValue: value,
|
||||
isBursted,
|
||||
expireAt: timestamp + REACTIONS_BURST_WINDOW,
|
||||
@ -1489,9 +1475,9 @@ function useReactionsToast(props: UseReactionsToastType): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const unburstedEmojis = new Map<string, Set<string>>();
|
||||
const unburstedEmojis = new Map<Emoji, Set<string>>();
|
||||
const unburstedEmojisReactorIds = new Map<
|
||||
string,
|
||||
Emoji,
|
||||
Set<ServiceIdString | number>
|
||||
>();
|
||||
reactionsShown.current.forEach(
|
||||
@ -1505,6 +1491,10 @@ function useReactionsToast(props: UseReactionsToastType): void {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Emoji.isEmoji(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reactionKeys = unburstedEmojis.get(value) ?? new Set();
|
||||
reactionKeys.add(key);
|
||||
unburstedEmojis.set(value, reactionKeys);
|
||||
@ -1540,7 +1530,7 @@ function useReactionsToast(props: UseReactionsToastType): void {
|
||||
}
|
||||
|
||||
burstsShown.current.set(value, time);
|
||||
const values: Array<string> = [];
|
||||
const values: Array<Emoji> = [];
|
||||
reactionKeys.forEach(key => {
|
||||
const reactionShown = reactionsShown.current.get(key);
|
||||
if (!reactionShown) {
|
||||
@ -1565,7 +1555,6 @@ function useReactionsToast(props: UseReactionsToastType): void {
|
||||
localDemuxId,
|
||||
i18n,
|
||||
ourServiceId,
|
||||
emojiLocalizer,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -16,8 +16,8 @@ import type { ContactNameColorType } from '../types/Colors.std.ts';
|
||||
import { ContactNameColors, ConversationColors } from '../types/Colors.std.ts';
|
||||
import { getDefaultConversation } from '../test-helpers/getDefaultConversation.std.ts';
|
||||
import { PaymentEventKind } from '../types/Payment.std.ts';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import { isNotNil } from '../util/isNotNil.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -29,12 +29,12 @@ const groupAdmins = [
|
||||
},
|
||||
{
|
||||
member: getDefaultConversation(),
|
||||
labelEmoji: '✅',
|
||||
labelEmoji: Emoji.CHECKMARK,
|
||||
labelString: 'Planner',
|
||||
},
|
||||
{
|
||||
member: getDefaultConversation(),
|
||||
labelEmoji: '#',
|
||||
labelEmoji: Emoji.unsafeCastMaybeInvalidStringToVariant('#'),
|
||||
labelString: 'Invalid Emoji',
|
||||
},
|
||||
{
|
||||
@ -128,7 +128,7 @@ export default {
|
||||
sortedGroupMembers: [],
|
||||
// FunPicker
|
||||
onSelectEmoji: action('onSelectEmoji'),
|
||||
emojiSkinToneDefault: EmojiSkinTone.Type1,
|
||||
emojiSkinToneDefault: Emoji.SkinTone.Type1,
|
||||
pushPanelForConversation: action('pushPanelForConversation'),
|
||||
sendStickerMessage: action('sendStickerMessage'),
|
||||
// Message Requests
|
||||
|
||||
@ -82,7 +82,6 @@ import type { FunGifSelection } from './fun/panels/FunPanelGifs.dom.tsx';
|
||||
import type { SmartDraftGifMessageSendModalProps } from '../state/smart/DraftGifMessageSendModal.preload.tsx';
|
||||
import { strictAssert } from '../util/assert.std.ts';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog.dom.tsx';
|
||||
import type { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import { FunPickerButton } from './fun/FunButton.dom.tsx';
|
||||
import { AxoDropdownMenu } from '../axo/AxoDropdownMenu.dom.tsx';
|
||||
import { AxoIconButton } from '../axo/AxoIconButton.dom.tsx';
|
||||
@ -92,6 +91,7 @@ import { PollCreateModal } from './PollCreateModal.dom.tsx';
|
||||
import { useDocumentKeyDown } from '../hooks/useDocumentKeyDown.dom.ts';
|
||||
import { hasDraft } from '../util/hasDraft.std.ts';
|
||||
import type { ContactNameColorType } from '../types/Colors.std.ts';
|
||||
import type { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
export type OwnProps = Readonly<{
|
||||
acceptedMessageRequest: boolean | null;
|
||||
@ -123,7 +123,7 @@ export type OwnProps = Readonly<{
|
||||
focusCounter: number;
|
||||
groupAdmins: Array<{
|
||||
member: ConversationType;
|
||||
labelEmoji: string | undefined;
|
||||
labelEmoji: Emoji.Variant | undefined;
|
||||
labelString: string | undefined;
|
||||
}>;
|
||||
groupVersion: 1 | 2 | null;
|
||||
@ -229,7 +229,7 @@ export type OwnProps = Readonly<{
|
||||
) => void;
|
||||
|
||||
onSelectEmoji: (emojiSelection: FunEmojiSelection) => void;
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
emojiSkinToneDefault: Emoji.SkinTone | null;
|
||||
}>;
|
||||
|
||||
export type Props = Pick<
|
||||
|
||||
@ -8,8 +8,8 @@ import { getDefaultConversation } from '../test-helpers/getDefaultConversation.s
|
||||
import type { Props } from './CompositionInput.dom.tsx';
|
||||
import { CompositionInput } from './CompositionInput.dom.tsx';
|
||||
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext.std.ts';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import { generateAci } from '../test-helpers/serviceIdUtils.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -46,7 +46,7 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => {
|
||||
sendCounter: 0,
|
||||
sortedGroupMembers: overrideProps.sortedGroupMembers ?? [],
|
||||
emojiSkinToneDefault:
|
||||
overrideProps.emojiSkinToneDefault ?? EmojiSkinTone.None,
|
||||
overrideProps.emojiSkinToneDefault ?? Emoji.SkinTone.None,
|
||||
theme: useContext(StorybookThemeContext),
|
||||
inputApi: null,
|
||||
shouldHidePopovers: null,
|
||||
|
||||
@ -88,19 +88,14 @@ import type { AutoSubstituteAsciiEmojisOptions } from '../quill/auto-substitute-
|
||||
import { AutoSubstituteAsciiEmojis } from '../quill/auto-substitute-ascii-emojis/index.dom.tsx';
|
||||
import { dropNull } from '../util/dropNull.std.ts';
|
||||
import { SimpleQuillWrapper } from './SimpleQuillWrapper.dom.tsx';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
type EmojiSkinTone,
|
||||
} from './fun/data/emojis.std.ts';
|
||||
import { FUN_STATIC_EMOJI_CLASS } from './fun/FunEmoji.dom.tsx';
|
||||
import { useFunEmojiSearch } from './fun/useFunEmojiSearch.dom.tsx';
|
||||
import type { EmojiCompletionOptions } from '../quill/emoji/completion.dom.tsx';
|
||||
import { useFunEmojiLocalizer } from './fun/useFunEmojiLocalizer.dom.tsx';
|
||||
import { MAX_BODY_ATTACHMENT_BYTE_LENGTH } from '../util/longAttachment.std.ts';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.dom.tsx';
|
||||
import { AxoSymbol } from '../axo/AxoSymbol.dom.tsx';
|
||||
import { AxoTooltip } from '../axo/AxoTooltip.dom.tsx';
|
||||
import { tw } from '../axo/tw.dom.tsx';
|
||||
import type { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const log = createLogger('CompositionInput');
|
||||
|
||||
@ -145,7 +140,7 @@ export type Props = Readonly<{
|
||||
isFormattingEnabled: boolean;
|
||||
isActive: boolean;
|
||||
sendCounter: number;
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
emojiSkinToneDefault: Emoji.SkinTone | null;
|
||||
draftText: string | null;
|
||||
draftBodyRanges: HydratedBodyRangesType | null;
|
||||
moduleClassName?: string;
|
||||
@ -321,12 +316,10 @@ export function CompositionInput(props: Props): ReactElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const emojiVariant = getEmojiVariantByKey(emojiSelection.variantKey);
|
||||
|
||||
const delta = new Delta()
|
||||
.retain(insertionRange.index)
|
||||
.delete(insertionRange.length)
|
||||
.insert({ emoji: { value: emojiVariant.value } });
|
||||
.insert({ emoji: { value: emojiSelection.emoji } });
|
||||
|
||||
quill.updateContents(delta, 'user');
|
||||
quill.setSelection(insertionRange.index + 1, 0, 'user');
|
||||
@ -811,9 +804,6 @@ export function CompositionInput(props: Props): ReactElement {
|
||||
const callbacksRef = useRef(unstaleCallbacks);
|
||||
callbacksRef.current = unstaleCallbacks;
|
||||
|
||||
const emojiSearch = useFunEmojiSearch();
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
|
||||
const reactQuill = useMemo(
|
||||
() => {
|
||||
const delta = generateDelta(draftText || '', draftBodyRanges || []);
|
||||
@ -879,8 +869,6 @@ export function CompositionInput(props: Props): ReactElement {
|
||||
onSelectEmoji: (emojiSelection: FunEmojiSelection) =>
|
||||
callbacksRef.current.onSelectEmoji(emojiSelection),
|
||||
emojiSkinToneDefault,
|
||||
emojiSearch,
|
||||
emojiLocalizer,
|
||||
} satisfies EmojiCompletionOptions,
|
||||
autoSubstituteAsciiEmojis: {
|
||||
emojiSkinToneDefault,
|
||||
|
||||
@ -14,8 +14,8 @@ import type { PreferredBadgeSelectorType } from '../state/selectors/badges.prelo
|
||||
import * as grapheme from '../util/grapheme.std.ts';
|
||||
import { FunEmojiPicker } from './fun/FunEmojiPicker.dom.tsx';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.dom.tsx';
|
||||
import type { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import { FunEmojiPickerButton } from './fun/FunButton.dom.tsx';
|
||||
import type { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
export type CompositionTextAreaProps = {
|
||||
bodyRanges: HydratedBodyRangesType | null;
|
||||
@ -32,8 +32,8 @@ export type CompositionTextAreaProps = {
|
||||
draftBodyRanges: HydratedBodyRangesType,
|
||||
caretLocation?: number
|
||||
) => void;
|
||||
emojiSkinToneDefault: EmojiSkinTone;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinToneDefault: EmojiSkinTone) => void;
|
||||
emojiSkinToneDefault: Emoji.SkinTone;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinToneDefault: Emoji.SkinTone) => void;
|
||||
onSubmit: (
|
||||
message: string,
|
||||
draftBodyRanges: DraftBodyRanges,
|
||||
|
||||
@ -16,6 +16,7 @@ import { getDefaultConversation } from '../test-helpers/getDefaultConversation.s
|
||||
import { ThemeType } from '../types/Util.std.ts';
|
||||
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext.std.ts';
|
||||
import { makeFakeLookupConversationWithoutServiceId } from '../test-helpers/fakeLookupConversationWithoutServiceId.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { times, omit } = lodash;
|
||||
|
||||
@ -349,7 +350,7 @@ export const ConversationWithDraft = (): JSX.Element =>
|
||||
shouldShowDraft: true,
|
||||
draftPreview: {
|
||||
text: "I'm in the middle of typing this...",
|
||||
prefix: '🎤',
|
||||
prefix: Emoji.MICROPHONE,
|
||||
bodyRanges: [],
|
||||
},
|
||||
});
|
||||
|
||||
@ -5,10 +5,9 @@ import type { ComponentProps, JSX } from 'react';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../reactions/constants.std.ts';
|
||||
import type { PropsType } from './CustomizingPreferredReactionsModal.dom.tsx';
|
||||
import { CustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal.dom.tsx';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -22,19 +21,28 @@ const defaultProps: ComponentProps<typeof CustomizingPreferredReactionsModal> =
|
||||
'cancelCustomizePreferredReactionsModal'
|
||||
),
|
||||
deselectDraftEmoji: action('deselectDraftEmoji'),
|
||||
draftPreferredReactions: ['✨', '❇️', '🎇', '🦈', '💖', '🅿️'],
|
||||
draftPreferredReactions: [
|
||||
Emoji.SPARKLES,
|
||||
Emoji.SPARKLE,
|
||||
Emoji.FIREWORK_SPARKLER,
|
||||
Emoji.SHARK,
|
||||
Emoji.SPARKLING_HEART,
|
||||
Emoji.PARKING,
|
||||
],
|
||||
hadSaveError: false,
|
||||
i18n,
|
||||
isSaving: false,
|
||||
onEmojiSkinToneDefaultChange: action('onEmojiSkinToneDefaultChange'),
|
||||
originalPreferredReactions: DEFAULT_PREFERRED_REACTION_EMOJI,
|
||||
recentEmojis: ['cake'],
|
||||
originalPreferredReactions: Emoji.getDefaultPreferredReactionEmojis(
|
||||
Emoji.SkinTone.None
|
||||
),
|
||||
recentEmojis: [Emoji.CAKE],
|
||||
replaceSelectedDraftEmoji: action('replaceSelectedDraftEmoji'),
|
||||
resetDraftEmoji: action('resetDraftEmoji'),
|
||||
savePreferredReactions: action('savePreferredReactions'),
|
||||
selectDraftEmojiToBeReplaced: action('selectDraftEmojiToBeReplaced'),
|
||||
selectedDraftEmojiIndex: undefined,
|
||||
emojiSkinToneDefault: EmojiSkinTone.Type4,
|
||||
emojiSkinToneDefault: Emoji.SkinTone.Type4,
|
||||
};
|
||||
|
||||
export function Default(): JSX.Element {
|
||||
|
||||
@ -12,31 +12,26 @@ import {
|
||||
ReactionPickerPickerEmojiButton,
|
||||
ReactionPickerPickerStyle,
|
||||
} from './ReactionPickerPicker.dom.tsx';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS } from '../reactions/constants.std.ts';
|
||||
import {
|
||||
EmojiSkinTone,
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
} from './fun/data/emojis.std.ts';
|
||||
import { FunEmojiPicker } from './fun/FunEmojiPicker.dom.tsx';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.dom.tsx';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { isEqual } = lodash;
|
||||
|
||||
export type PropsType = {
|
||||
draftPreferredReactions: ReadonlyArray<string>;
|
||||
draftPreferredReactions: ReadonlyArray<Emoji.Variant>;
|
||||
hadSaveError: boolean;
|
||||
i18n: LocalizerType;
|
||||
isSaving: boolean;
|
||||
originalPreferredReactions: ReadonlyArray<string>;
|
||||
recentEmojis: ReadonlyArray<string>;
|
||||
originalPreferredReactions: ReadonlyArray<Emoji.Variant>;
|
||||
recentEmojis: ReadonlyArray<Emoji.Parent>;
|
||||
selectedDraftEmojiIndex: undefined | number;
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
emojiSkinToneDefault: Emoji.SkinTone | null;
|
||||
|
||||
cancelCustomizePreferredReactionsModal(): unknown;
|
||||
deselectDraftEmoji(): unknown;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinToneDefault: EmojiSkinTone) => void;
|
||||
replaceSelectedDraftEmoji(newEmoji: string): unknown;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinToneDefault: Emoji.SkinTone) => void;
|
||||
replaceSelectedDraftEmoji(newEmoji: Emoji.Variant): unknown;
|
||||
resetDraftEmoji(): unknown;
|
||||
savePreferredReactions(): unknown;
|
||||
selectDraftEmojiToBeReplaced(index: number): unknown;
|
||||
@ -68,13 +63,9 @@ export function CustomizingPreferredReactionsModal({
|
||||
const canReset =
|
||||
!isSaving &&
|
||||
!isEqual(
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS.map(parentKey => {
|
||||
const variant = getEmojiVariantByParentKeyAndSkinTone(
|
||||
parentKey,
|
||||
emojiSkinToneDefault ?? EmojiSkinTone.None
|
||||
);
|
||||
return variant.value;
|
||||
}),
|
||||
Emoji.getDefaultPreferredReactionEmojis(
|
||||
emojiSkinToneDefault ?? Emoji.SkinTone.None
|
||||
),
|
||||
draftPreferredReactions
|
||||
);
|
||||
const canSave = !isSaving && hasChanged;
|
||||
@ -147,10 +138,7 @@ export function CustomizingPreferredReactionsModal({
|
||||
deselectDraftEmoji();
|
||||
}}
|
||||
onSelectEmoji={emojiSelection => {
|
||||
const emojiVariant = getEmojiVariantByKey(
|
||||
emojiSelection.variantKey
|
||||
);
|
||||
replaceSelectedDraftEmoji(emojiVariant.value);
|
||||
replaceSelectedDraftEmoji(emojiSelection.emoji);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -165,7 +153,7 @@ export function CustomizingPreferredReactionsModal({
|
||||
}
|
||||
|
||||
function CustomizingPreferredReactionsModalItem(props: {
|
||||
emoji: string;
|
||||
emoji: Emoji.Variant;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
onDeselect: () => void;
|
||||
|
||||
@ -10,10 +10,10 @@ import {
|
||||
import { ThemeType } from '../types/Util.std.ts';
|
||||
import { CompositionTextArea } from './CompositionTextArea.dom.tsx';
|
||||
import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea.preload.tsx';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import { LoadingState } from '../util/loadable.std.ts';
|
||||
import { VIDEO_MP4 } from '../types/MIME.std.ts';
|
||||
import { drop } from '../util/drop.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -39,7 +39,7 @@ function RenderCompositionTextArea(props: SmartCompositionTextAreaProps) {
|
||||
onTextTooLong={action('onTextTooLong')}
|
||||
ourConversationId="me"
|
||||
platform="darwin"
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
emojiSkinToneDefault={Emoji.SkinTone.None}
|
||||
convertDraftBodyRangesIntoHydrated={() => []}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -8,11 +8,11 @@ import { Modal } from './Modal.dom.tsx';
|
||||
import type { HydratedBodyRangesType } from '../types/BodyRange.std.ts';
|
||||
import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea.preload.tsx';
|
||||
import type { ThemeType } from '../types/Util.std.ts';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import { FunGifPreview } from './fun/FunGif.dom.tsx';
|
||||
import type { FunGifSelection } from './fun/panels/FunPanelGifs.dom.tsx';
|
||||
import type { GifDownloadState } from '../state/smart/DraftGifMessageSendModal.preload.tsx';
|
||||
import { LoadingState } from '../util/loadable.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
export type DraftGifMessageSendModalProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
@ -98,7 +98,7 @@ export function DraftGifMessageSendModal(
|
||||
onChange={props.onChange}
|
||||
onSubmit={props.onSubmit}
|
||||
theme={props.theme}
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
emojiSkinToneDefault={Emoji.SkinTone.None}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@ -15,7 +15,7 @@ import { getDefaultConversation } from '../test-helpers/getDefaultConversation.s
|
||||
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext.std.ts';
|
||||
import { CompositionTextArea } from './CompositionTextArea.dom.tsx';
|
||||
import type { MessageForwardDraft } from '../types/ForwardDraft.std.ts';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const createAttachment = (
|
||||
props: Partial<AttachmentForUIType> = {}
|
||||
@ -69,7 +69,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
onTextTooLong={action('onTextTooLong')}
|
||||
ourConversationId="me"
|
||||
platform="darwin"
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
emojiSkinToneDefault={Emoji.SkinTone.None}
|
||||
convertDraftBodyRangesIntoHydrated={() => []}
|
||||
/>
|
||||
),
|
||||
|
||||
@ -43,7 +43,7 @@ import {
|
||||
} from '../types/ForwardDraft.std.ts';
|
||||
import { missingCaseError } from '../util/missingCaseError.std.ts';
|
||||
import { Theme } from '../util/theme.std.ts';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
export enum ForwardMessagesModalType {
|
||||
Forward,
|
||||
@ -477,7 +477,7 @@ function ForwardMessageEditor({
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
theme={theme}
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
emojiSkinToneDefault={Emoji.SkinTone.None}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -75,7 +75,7 @@ export function MultipleTagReplacement(
|
||||
);
|
||||
}
|
||||
|
||||
export function Emoji(
|
||||
export function WithEmoji(
|
||||
args: Props<'icu:Message__reaction-emoji-label--you'>
|
||||
): JSX.Element {
|
||||
return (
|
||||
|
||||
@ -5,7 +5,7 @@ import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { PropsType } from './MediaEditor.dom.tsx';
|
||||
import { MediaEditor } from './MediaEditor.dom.tsx';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
const IMAGE_1 = '/fixtures/nathan-anderson-316188-unsplash.jpg';
|
||||
@ -30,7 +30,7 @@ export default {
|
||||
onSelectEmoji: action('onSelectEmoji'),
|
||||
onTextTooLong: action('onTextTooLong'),
|
||||
platform: 'darwin',
|
||||
emojiSkinToneDefault: EmojiSkinTone.None,
|
||||
emojiSkinToneDefault: Emoji.SkinTone.None,
|
||||
convertDraftBodyRangesIntoHydrated: () => undefined,
|
||||
},
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
@ -17,6 +17,7 @@ import { HOUR } from '../util/durations/index.std.ts';
|
||||
import { AxoDropdownMenu } from '../axo/AxoDropdownMenu.dom.tsx';
|
||||
import { AxoButton } from '../axo/AxoButton.dom.tsx';
|
||||
import type { ConversationType } from '../state/ducks/conversations.preload.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -34,7 +35,7 @@ const threeProfiles = [
|
||||
{
|
||||
id: 'Weekday' as NotificationProfileIdString,
|
||||
name: 'Weekday',
|
||||
emoji: '😬',
|
||||
emoji: Emoji.GRIMACING,
|
||||
color: 0xffe3e3fe,
|
||||
|
||||
createdAtMs: Date.now(),
|
||||
@ -63,7 +64,7 @@ const threeProfiles = [
|
||||
{
|
||||
id: 'Weekend' as NotificationProfileIdString,
|
||||
name: 'Weekend',
|
||||
emoji: '❤️🔥',
|
||||
emoji: Emoji.HEART_ON_FIRE,
|
||||
color: 0xffd7d7d9,
|
||||
|
||||
createdAtMs: Date.now(),
|
||||
|
||||
@ -21,7 +21,6 @@ import { Toast } from './Toast.dom.tsx';
|
||||
import { FunEmojiPicker } from './fun/FunEmojiPicker.dom.tsx';
|
||||
import { FunEmojiPickerButton } from './fun/FunButton.dom.tsx';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.dom.tsx';
|
||||
import { getEmojiVariantByKey } from './fun/data/emojis.std.ts';
|
||||
import { strictAssert } from '../util/assert.std.ts';
|
||||
import {
|
||||
type PollCreateType,
|
||||
@ -178,8 +177,7 @@ export function PollCreateModal({
|
||||
strictAssert(inputEl, 'Missing input ref for option');
|
||||
|
||||
const { selectionStart, selectionEnd } = inputEl;
|
||||
const variant = getEmojiVariantByKey(emojiSelection.variantKey);
|
||||
const emoji = variant.value;
|
||||
const emoji = emojiSelection.emoji;
|
||||
|
||||
const updatedOptions = options.map(opt => {
|
||||
if (opt.id !== optionId) {
|
||||
|
||||
@ -12,7 +12,6 @@ import { DEFAULT_CONVERSATION_COLOR } from '../types/Colors.std.ts';
|
||||
import { PhoneNumberSharingMode } from '../types/PhoneNumberSharingMode.std.ts';
|
||||
import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability.std.ts';
|
||||
import { sleep } from '../util/sleep.std.ts';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import {
|
||||
DAY,
|
||||
DurationInSeconds,
|
||||
@ -61,6 +60,7 @@ import type { ExternalProps as SmartNotificationProfilesProps } from '../state/s
|
||||
import type { NotificationProfileIdString } from '../types/NotificationProfile.std.ts';
|
||||
import type { ExportResultType } from '../services/backups/types.std.ts';
|
||||
import { BackupLevel } from '../services/backups/types.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { shuffle } = lodash;
|
||||
|
||||
@ -430,7 +430,7 @@ export default {
|
||||
customColors: {},
|
||||
defaultConversationColor: DEFAULT_CONVERSATION_COLOR,
|
||||
deviceName: 'Work Windows ME',
|
||||
emojiSkinToneDefault: EmojiSkinTone.None,
|
||||
emojiSkinToneDefault: Emoji.SkinTone.None,
|
||||
phoneNumber: '+1 555 123-4567',
|
||||
hasAnyCurrentCustomChatFolders: false,
|
||||
hasAudioNotifications: true,
|
||||
@ -757,7 +757,7 @@ const threeProfiles = [
|
||||
{
|
||||
id: 'Weekday' as NotificationProfileIdString,
|
||||
name: 'Weekday',
|
||||
emoji: '😬',
|
||||
emoji: Emoji.GRIMACING,
|
||||
color: 0xffe3e3fe,
|
||||
|
||||
createdAtMs: Date.now(),
|
||||
@ -786,7 +786,7 @@ const threeProfiles = [
|
||||
{
|
||||
id: 'Weekend' as NotificationProfileIdString,
|
||||
name: 'Weekend',
|
||||
emoji: '❤️🔥',
|
||||
emoji: Emoji.HEART_ON_FIRE,
|
||||
color: 0xffd7d7d9,
|
||||
|
||||
createdAtMs: Date.now(),
|
||||
|
||||
@ -39,7 +39,6 @@ import { removeDiacritics } from '../util/removeDiacritics.std.ts';
|
||||
import { assertDev } from '../util/assert.std.ts';
|
||||
import { I18n } from './I18n.dom.tsx';
|
||||
import { FunSkinTonesList } from './fun/FunSkinTones.dom.tsx';
|
||||
import { EMOJI_PARENT_KEY_CONSTANTS } from './fun/data/emojis.std.ts';
|
||||
import {
|
||||
SettingsControl as Control,
|
||||
FlowingSettingsControl as FlowingControl,
|
||||
@ -48,14 +47,12 @@ import {
|
||||
} from './PreferencesUtil.dom.tsx';
|
||||
import { PreferencesBackups } from './PreferencesBackups.dom.tsx';
|
||||
import { PreferencesInternal } from './PreferencesInternal.dom.tsx';
|
||||
import { FunEmojiLocalizationProvider } from './fun/FunEmojiLocalizationProvider.dom.tsx';
|
||||
import { Avatar, AvatarSize } from './Avatar.dom.tsx';
|
||||
import { NavSidebar } from './NavSidebar.dom.tsx';
|
||||
import type { SettingsLocation } from '../types/Nav.std.ts';
|
||||
import { SettingsPage, ProfileEditorPage } from '../types/Nav.std.ts';
|
||||
import { tw } from '../axo/tw.dom.tsx';
|
||||
import { FullWidthButton } from './PreferencesNotificationProfiles.dom.tsx';
|
||||
import type { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import type { MediaDeviceSettings } from '../types/Calling.std.ts';
|
||||
import type { ValidationResultType as BackupValidationResultType } from '../services/backups/index.preload.ts';
|
||||
import type {
|
||||
@ -105,6 +102,7 @@ import { isDonationsPage } from './PreferencesDonations.dom.tsx';
|
||||
import type { VisibleRemoteMegaphoneType } from '../types/Megaphone.std.ts';
|
||||
import { TitlebarDragArea } from './TitlebarDragArea.dom.tsx';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges.preload.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { isNumber, noop, partition } = lodash;
|
||||
|
||||
@ -134,7 +132,7 @@ export type PropsDataType = {
|
||||
customColors: Record<string, CustomColorType>;
|
||||
defaultConversationColor: DefaultConversationColorType;
|
||||
deviceName?: string;
|
||||
emojiSkinToneDefault: EmojiSkinTone;
|
||||
emojiSkinToneDefault: Emoji.SkinTone;
|
||||
hasAnyCurrentCustomChatFolders: boolean;
|
||||
hasAudioNotifications?: boolean;
|
||||
hasAutoConvertEmoji: boolean;
|
||||
@ -326,7 +324,7 @@ type PropsFunctionType = {
|
||||
onCallRingtoneNotificationChange: CheckboxChangeHandlerType;
|
||||
onContentProtectionChange: CheckboxChangeHandlerType;
|
||||
onCountMutedConversationsChange: CheckboxChangeHandlerType;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinTone: Emoji.SkinTone) => void;
|
||||
onHasKeyTransparencyDisabledChanged: SelectChangeHandlerType<boolean>;
|
||||
onHasStoriesDisabledChanged: SelectChangeHandlerType<boolean>;
|
||||
onHideMenuBarChange: CheckboxChangeHandlerType;
|
||||
@ -1254,7 +1252,7 @@ export function Preferences({
|
||||
right={
|
||||
<FunSkinTonesList
|
||||
i18n={i18n}
|
||||
emoji={EMOJI_PARENT_KEY_CONSTANTS.RAISED_HAND}
|
||||
emoji={Emoji.HAND}
|
||||
skinTone={emojiSkinToneDefault}
|
||||
onSelectSkinTone={onEmojiSkinToneDefaultChange}
|
||||
/>
|
||||
@ -2585,7 +2583,7 @@ export function Preferences({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FunEmojiLocalizationProvider i18n={i18n}>
|
||||
<>
|
||||
<div className="Preferences">
|
||||
<NavSidebar
|
||||
title={i18n('icu:Preferences--header')}
|
||||
@ -2828,7 +2826,7 @@ export function Preferences({
|
||||
{content}
|
||||
</div>
|
||||
<TitlebarDragArea />
|
||||
</FunEmojiLocalizationProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -9,14 +9,8 @@ import { sample, isEqual, noop, range } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { Popper } from 'react-popper';
|
||||
|
||||
import {
|
||||
isEmojiVariantValue,
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
} from './fun/data/emojis.std.ts';
|
||||
import { FunStaticEmoji } from './fun/FunEmoji.dom.tsx';
|
||||
import { FunEmojiPicker } from './fun/FunEmojiPicker.dom.tsx';
|
||||
import { useFunEmojiLocalizer } from './fun/useFunEmojiLocalizer.dom.tsx';
|
||||
import { FunEmojiPickerButton } from './fun/FunButton.dom.tsx';
|
||||
import { tw } from '../axo/tw.dom.tsx';
|
||||
import { AxoButton } from '../axo/AxoButton.dom.tsx';
|
||||
@ -46,8 +40,6 @@ import { useRefMerger } from '../hooks/useRefMerger.std.ts';
|
||||
import { handleOutsideClick } from '../util/handleOutsideClick.dom.ts';
|
||||
import { useEscapeHandling } from '../hooks/useEscapeHandling.dom.ts';
|
||||
import { Modal } from './Modal.dom.tsx';
|
||||
|
||||
import type { EmojiVariantKey } from './fun/data/emojis.std.ts';
|
||||
import type { LocalizerType } from '../types/I18N.std.ts';
|
||||
import type { ThemeType } from '../types/Util.std.ts';
|
||||
import type { ConversationType } from '../state/ducks/conversations.preload.ts';
|
||||
@ -60,6 +52,7 @@ import type {
|
||||
} from '../types/NotificationProfile.std.ts';
|
||||
import type { SettingsLocation } from '../types/Nav.std.ts';
|
||||
import { addLeadingZero } from '../util/timestamp.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
enum CreateFlowPage {
|
||||
Name = 'Name',
|
||||
@ -261,14 +254,6 @@ function getColorFromProfile(argb: number): string {
|
||||
return `#${rgb.toString(16)}`;
|
||||
}
|
||||
|
||||
function getEmojiVariantKey(value: string): EmojiVariantKey | undefined {
|
||||
if (isEmojiVariantValue(value)) {
|
||||
return getEmojiVariantKeyByValue(value);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type ProfileToSave = Omit<NotificationProfileType, 'id'>;
|
||||
|
||||
export function NotificationProfilesCreateFlow({
|
||||
@ -284,7 +269,7 @@ export function NotificationProfilesCreateFlow({
|
||||
const [page, setPage] = useState(CreateFlowPage.Name);
|
||||
|
||||
const [name, setName] = useState<string | undefined>();
|
||||
const [emoji, setEmoji] = useState<string | undefined>();
|
||||
const [emoji, setEmoji] = useState<Emoji.Variant | undefined>();
|
||||
const [allowedMembers, setAllowedMembers] = useState<ReadonlySet<string>>(
|
||||
new Set<string>()
|
||||
);
|
||||
@ -683,40 +668,39 @@ function NotificationProfilesNamePage({
|
||||
}: {
|
||||
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
||||
i18n: LocalizerType;
|
||||
initialEmoji: string | undefined;
|
||||
initialEmoji: Emoji.Variant | undefined;
|
||||
initialName?: string;
|
||||
isEditing: boolean;
|
||||
onBack: VoidFunction;
|
||||
onNext: () => void;
|
||||
onUpdate: (data: { emoji: string | undefined; name: string }) => void;
|
||||
onUpdate: (data: { emoji: Emoji.Variant | undefined; name: string }) => void;
|
||||
theme: ThemeType;
|
||||
}) {
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
|
||||
const [name, setName] = useState(initialName);
|
||||
const [emoji, setEmoji] = useState<string | undefined>(initialEmoji);
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
const [emoji, setEmoji] = useState<Emoji.Variant | undefined>(initialEmoji);
|
||||
|
||||
const isValid = Boolean(name);
|
||||
const sampleProfileNames = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
emoji: '💪',
|
||||
emoji: Emoji.getDefaultVariant(Emoji.MUSCLE),
|
||||
text: i18n('icu:NotificationProfiles--sample-name__work'),
|
||||
},
|
||||
{
|
||||
emoji: '😴',
|
||||
emoji: Emoji.SLEEPING,
|
||||
text: i18n('icu:NotificationProfiles--sample-name__sleep'),
|
||||
},
|
||||
{
|
||||
emoji: '🚗',
|
||||
emoji: Emoji.CAR,
|
||||
text: i18n('icu:NotificationProfiles--sample-name__driving'),
|
||||
},
|
||||
{
|
||||
emoji: '😊',
|
||||
emoji: Emoji.BLUSH,
|
||||
text: i18n('icu:NotificationProfiles--sample-name__downtime'),
|
||||
},
|
||||
{
|
||||
emoji: '💡',
|
||||
emoji: Emoji.BULB,
|
||||
text: i18n('icu:NotificationProfiles--sample-name__focus'),
|
||||
},
|
||||
] as const;
|
||||
@ -739,8 +723,6 @@ function NotificationProfilesNamePage({
|
||||
[emoji, setEmoji, setName, onUpdate]
|
||||
);
|
||||
|
||||
const emojiKey = emoji ? getEmojiVariantKey(emoji) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
@ -767,8 +749,7 @@ function NotificationProfilesNamePage({
|
||||
onOpenChange={handleFunEmojiPickerOpenChange}
|
||||
placement="bottom"
|
||||
onSelectEmoji={data => {
|
||||
const newEmoji = getEmojiVariantByKey(data.variantKey)?.value;
|
||||
|
||||
const newEmoji = data.emoji;
|
||||
setEmoji(newEmoji);
|
||||
if (name) {
|
||||
onUpdate({ name, emoji: newEmoji });
|
||||
@ -777,7 +758,7 @@ function NotificationProfilesNamePage({
|
||||
closeOnSelect
|
||||
theme={theme}
|
||||
>
|
||||
<FunEmojiPickerButton i18n={i18n} selectedEmoji={emojiKey} />
|
||||
<FunEmojiPickerButton i18n={i18n} selectedEmoji={emoji} />
|
||||
</FunEmojiPicker>
|
||||
}
|
||||
maxLengthCount={140}
|
||||
@ -791,13 +772,6 @@ function NotificationProfilesNamePage({
|
||||
/>
|
||||
<div className={tw('mx-auto w-full max-w-[320px]')}>
|
||||
{sampleProfileNames.map(item => {
|
||||
const itemEmojiKey = getEmojiVariantKey(item.emoji);
|
||||
strictAssert(
|
||||
itemEmojiKey,
|
||||
'Emoji for name defaults should exist'
|
||||
);
|
||||
const itemEmojiData = getEmojiVariantByKey(itemEmojiKey);
|
||||
|
||||
return (
|
||||
<FullWidthButton
|
||||
key={item.text}
|
||||
@ -813,11 +787,9 @@ function NotificationProfilesNamePage({
|
||||
>
|
||||
<FunStaticEmoji
|
||||
role="img"
|
||||
aria-label={emojiLocalizer.getLocaleShortName(
|
||||
itemEmojiData.key
|
||||
)}
|
||||
aria-label={Emoji.getDisplayLabel(item.emoji)}
|
||||
size={24}
|
||||
emoji={itemEmojiData}
|
||||
emoji={item.emoji}
|
||||
/>
|
||||
{item.text}
|
||||
</FullWidthButton>
|
||||
@ -1566,12 +1538,11 @@ function EmojiOrMoon({
|
||||
i18n,
|
||||
size,
|
||||
}: {
|
||||
emoji?: EmojiVariantKey | undefined;
|
||||
emoji?: Emoji.Variant | undefined;
|
||||
forceLightTheme?: boolean;
|
||||
i18n: LocalizerType;
|
||||
size: IconSize;
|
||||
}) {
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
const sizeMap = useMemo(
|
||||
() => ({
|
||||
large: 48 as const,
|
||||
@ -1605,17 +1576,15 @@ function EmojiOrMoon({
|
||||
);
|
||||
}
|
||||
|
||||
const emojiData = getEmojiVariantByKey(emoji);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={tw('absolute inset-s-1/2 top-1/2 -translate-1/2 leading-0')}
|
||||
>
|
||||
<FunStaticEmoji
|
||||
role="img"
|
||||
aria-label={emojiLocalizer.getLocaleShortName(emojiData.key)}
|
||||
aria-label={Emoji.getDisplayLabel(emoji)}
|
||||
size={sizeMap[size]}
|
||||
emoji={emojiData}
|
||||
emoji={emoji}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
@ -1775,7 +1744,6 @@ export function ProfileAvatar({
|
||||
profile?: ProfileToSave;
|
||||
size: IconSize;
|
||||
}): ReactNode {
|
||||
const emoji = profile?.emoji ? getEmojiVariantKey(profile.emoji) : undefined;
|
||||
const backgroundColor = profile?.color
|
||||
? getColorFromProfile(profile.color)
|
||||
: undefined;
|
||||
@ -1803,7 +1771,7 @@ export function ProfileAvatar({
|
||||
style={{ backgroundColor }}
|
||||
>
|
||||
<EmojiOrMoon
|
||||
emoji={emoji}
|
||||
emoji={profile?.emoji}
|
||||
forceLightTheme={forceLightTheme}
|
||||
i18n={i18n}
|
||||
size={size}
|
||||
|
||||
@ -19,7 +19,7 @@ import {
|
||||
} from '../state/ducks/usernameEnums.std.ts';
|
||||
import { getRandomColor } from '../test-helpers/getRandomColor.std.ts';
|
||||
import { SignalService as Proto } from '../protobuf/index.std.ts';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -47,7 +47,7 @@ export default {
|
||||
},
|
||||
},
|
||||
args: {
|
||||
aboutEmoji: '',
|
||||
aboutEmoji: undefined,
|
||||
aboutText: casual.sentence,
|
||||
profileAvatarUrl: undefined,
|
||||
conversationId: generateUuid(),
|
||||
@ -63,7 +63,7 @@ export default {
|
||||
usernameEditState: UsernameEditState.Editing,
|
||||
usernameLinkState: UsernameLinkState.Ready,
|
||||
|
||||
emojiSkinToneDefault: EmojiSkinTone.None,
|
||||
emojiSkinToneDefault: Emoji.SkinTone.None,
|
||||
userAvatarData: [],
|
||||
username: undefined,
|
||||
|
||||
@ -118,7 +118,7 @@ const Template: StoryFn<PropsType> = args => {
|
||||
|
||||
export const FullSet = Template.bind({});
|
||||
FullSet.args = {
|
||||
aboutEmoji: '🙏',
|
||||
aboutEmoji: Emoji.getDefaultVariant(Emoji.FOLDED_HANDS),
|
||||
aboutText: 'Live. Laugh. Love',
|
||||
familyName: casual.last_name,
|
||||
firstName: casual.first_name,
|
||||
@ -131,7 +131,7 @@ WithFullName.args = {
|
||||
};
|
||||
export const WithCustomAbout = Template.bind({});
|
||||
WithCustomAbout.args = {
|
||||
aboutEmoji: '🙏',
|
||||
aboutEmoji: Emoji.getDefaultVariant(Emoji.FOLDED_HANDS),
|
||||
aboutText: 'Live. Laugh. Love',
|
||||
};
|
||||
|
||||
|
||||
@ -28,17 +28,8 @@ import { Tooltip, TooltipPlacement } from './Tooltip.dom.tsx';
|
||||
import { offsetDistanceModifier } from '../util/popperUtil.std.ts';
|
||||
import { useReducedMotion } from '../hooks/useReducedMotion.dom.ts';
|
||||
import { FunStaticEmoji } from './fun/FunEmoji.dom.tsx';
|
||||
import {
|
||||
EMOJI_PARENT_KEY_CONSTANTS,
|
||||
EmojiSkinTone,
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from './fun/data/emojis.std.ts';
|
||||
import { FunEmojiPicker } from './fun/FunEmojiPicker.dom.tsx';
|
||||
import { FunEmojiPickerButton } from './fun/FunButton.dom.tsx';
|
||||
import { useFunEmojiLocalizer } from './fun/useFunEmojiLocalizer.dom.tsx';
|
||||
import { PreferencesContent } from './Preferences.dom.tsx';
|
||||
import { ProfileEditorPage } from '../types/Nav.std.ts';
|
||||
|
||||
@ -58,11 +49,11 @@ import type {
|
||||
} from '../state/ducks/conversations.preload.ts';
|
||||
import type { UsernameLinkState } from '../state/ducks/usernameEnums.std.ts';
|
||||
import type { ShowToastAction } from '../state/ducks/toast.preload.ts';
|
||||
import type { EmojiParentKey, EmojiVariantKey } from './fun/data/emojis.std.ts';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.dom.tsx';
|
||||
import { useConfirmDiscard } from '../hooks/useConfirmDiscard.dom.tsx';
|
||||
import { AxoButton } from '../axo/AxoButton.dom.tsx';
|
||||
import { normalizeProfileName } from '../util/normalizeProfileName.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
import { AxoTextField } from '../axo/AxoTextField.dom.tsx';
|
||||
import { tw } from '../axo/tw.dom.tsx';
|
||||
|
||||
@ -79,12 +70,12 @@ type PropsExternalType = {
|
||||
};
|
||||
|
||||
export type PropsDataType = {
|
||||
aboutEmoji?: string;
|
||||
aboutEmoji?: Emoji.Variant;
|
||||
aboutText?: string;
|
||||
color?: AvatarColorType;
|
||||
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
||||
conversationId: string;
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
emojiSkinToneDefault: Emoji.SkinTone | null;
|
||||
familyName?: string;
|
||||
firstName: string;
|
||||
hasCompletedUsernameLinkOnboarding: boolean;
|
||||
@ -120,42 +111,40 @@ export type PropsType = PropsDataType & PropsActionType & PropsExternalType;
|
||||
|
||||
type DefaultBio = {
|
||||
i18nLabel: string;
|
||||
emojiParentKey: EmojiParentKey;
|
||||
emojiParent: Emoji.Parent;
|
||||
};
|
||||
|
||||
function getDefaultBios(i18n: LocalizerType): Array<DefaultBio> {
|
||||
return [
|
||||
{
|
||||
i18nLabel: i18n('icu:Bio--speak-freely'),
|
||||
emojiParentKey: EMOJI_PARENT_KEY_CONSTANTS.WAVING_HAND,
|
||||
emojiParent: Emoji.WAVE,
|
||||
},
|
||||
{
|
||||
i18nLabel: i18n('icu:Bio--encrypted'),
|
||||
emojiParentKey: EMOJI_PARENT_KEY_CONSTANTS.ZIPPER_MOUTH_FACE,
|
||||
emojiParent: Emoji.ZIPPER_MOUTH_FACE,
|
||||
},
|
||||
{
|
||||
i18nLabel: i18n('icu:Bio--free-to-chat'),
|
||||
emojiParentKey: EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP,
|
||||
emojiParent: Emoji.THUMBS_UP,
|
||||
},
|
||||
{
|
||||
i18nLabel: i18n('icu:Bio--coffee-lover'),
|
||||
emojiParentKey: EMOJI_PARENT_KEY_CONSTANTS.HOT_BEVERAGE,
|
||||
emojiParent: Emoji.COFFEE,
|
||||
},
|
||||
{
|
||||
i18nLabel: i18n('icu:Bio--taking-break'),
|
||||
emojiParentKey: EMOJI_PARENT_KEY_CONSTANTS.MOBILE_PHONE_OFF,
|
||||
emojiParent: Emoji.MOBILE_PHONE_OFF,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function BioEmoji(props: { emoji: EmojiVariantKey }) {
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
const emojiVariant = getEmojiVariantByKey(props.emoji);
|
||||
function BioEmoji(props: { emoji: Emoji.Variant }) {
|
||||
return (
|
||||
<FunStaticEmoji
|
||||
role="img"
|
||||
aria-label={emojiLocalizer.getLocaleShortName(props.emoji)}
|
||||
emoji={emojiVariant}
|
||||
aria-label={Emoji.getDisplayLabel(props.emoji)}
|
||||
emoji={props.emoji}
|
||||
size={24}
|
||||
/>
|
||||
);
|
||||
@ -242,14 +231,14 @@ export function ProfileEditor({
|
||||
const [isResettingUsernameLink, setIsResettingUsernameLink] = useState(false);
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
|
||||
|
||||
const stagedAboutEmojiVariantKey = useMemo(() => {
|
||||
const stagedAboutEmojiVariant = useMemo(() => {
|
||||
if (
|
||||
stagedProfile.aboutEmoji == null ||
|
||||
!isEmojiVariantValue(stagedProfile.aboutEmoji)
|
||||
!Emoji.isEmoji(stagedProfile.aboutEmoji)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return getEmojiVariantKeyByValue(stagedProfile.aboutEmoji);
|
||||
return Emoji.ignorePreferredSkinTone(stagedProfile.aboutEmoji);
|
||||
}, [stagedProfile.aboutEmoji]);
|
||||
|
||||
// Reset username edit state when leaving
|
||||
@ -270,11 +259,9 @@ export function ProfileEditor({
|
||||
|
||||
const handleSelectEmoji = useCallback(
|
||||
(emojiSelection: FunEmojiSelection) => {
|
||||
const emojiVariant = getEmojiVariantByKey(emojiSelection.variantKey);
|
||||
|
||||
setStagedProfile(profileData => ({
|
||||
...profileData,
|
||||
aboutEmoji: emojiVariant.value,
|
||||
aboutEmoji: emojiSelection.emoji,
|
||||
}));
|
||||
},
|
||||
[setStagedProfile]
|
||||
@ -471,7 +458,7 @@ export function ProfileEditor({
|
||||
>
|
||||
<FunEmojiPickerButton
|
||||
i18n={i18n}
|
||||
selectedEmoji={stagedAboutEmojiVariantKey}
|
||||
selectedEmoji={stagedAboutEmojiVariant}
|
||||
/>
|
||||
</FunEmojiPicker>
|
||||
</div>
|
||||
@ -501,25 +488,25 @@ export function ProfileEditor({
|
||||
/>
|
||||
|
||||
{defaultBios.map(defaultBio => {
|
||||
const emojiVariant = getEmojiVariantByParentKeyAndSkinTone(
|
||||
defaultBio.emojiParentKey,
|
||||
emojiSkinToneDefault ?? EmojiSkinTone.None
|
||||
const emojiVariant = Emoji.getVariant(
|
||||
defaultBio.emojiParent,
|
||||
emojiSkinToneDefault ?? Emoji.SkinTone.None
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelRow
|
||||
className="ProfileEditor__row"
|
||||
key={defaultBio.emojiParentKey}
|
||||
key={defaultBio.emojiParent}
|
||||
icon={
|
||||
<div className="ProfileEditor__icon--container">
|
||||
<BioEmoji emoji={emojiVariant.key} />
|
||||
<BioEmoji emoji={emojiVariant} />
|
||||
</div>
|
||||
}
|
||||
label={defaultBio.i18nLabel}
|
||||
onClick={() => {
|
||||
setStagedProfile(profileData => ({
|
||||
...profileData,
|
||||
aboutEmoji: emojiVariant.value,
|
||||
aboutEmoji: emojiVariant,
|
||||
aboutText: defaultBio.i18nLabel,
|
||||
}));
|
||||
}}
|
||||
@ -754,10 +741,10 @@ export function ProfileEditor({
|
||||
<PanelRow
|
||||
className="ProfileEditor__row"
|
||||
icon={
|
||||
fullBio.aboutEmoji && isEmojiVariantValue(fullBio.aboutEmoji) ? (
|
||||
fullBio.aboutEmoji && Emoji.isEmoji(fullBio.aboutEmoji) ? (
|
||||
<div className="ProfileEditor__icon--container">
|
||||
<BioEmoji
|
||||
emoji={getEmojiVariantKeyByValue(fullBio.aboutEmoji)}
|
||||
emoji={Emoji.ignorePreferredSkinTone(fullBio.aboutEmoji)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@ -4,16 +4,10 @@
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Button } from 'react-aria-components';
|
||||
import { FunStaticEmoji } from './fun/FunEmoji.dom.tsx';
|
||||
import {
|
||||
getEmojiDebugLabel,
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from './fun/data/emojis.std.ts';
|
||||
import { createLogger } from '../logging/log.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const log = createLogger('ReactionPickerPicker');
|
||||
|
||||
@ -25,7 +19,7 @@ export enum ReactionPickerPickerStyle {
|
||||
export const ReactionPickerPickerEmojiButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
{
|
||||
emoji: string;
|
||||
emoji: Emoji.Variant;
|
||||
isSelected: boolean;
|
||||
onClick: () => unknown;
|
||||
title?: string;
|
||||
@ -34,16 +28,13 @@ export const ReactionPickerPickerEmojiButton = forwardRef<
|
||||
{ emoji, onClick, isSelected, title },
|
||||
ref
|
||||
) {
|
||||
if (!isEmojiVariantValue(emoji)) {
|
||||
if (!Emoji.isEmoji(emoji)) {
|
||||
log.error(
|
||||
`Expected a valid emoji variant value, got ${getEmojiDebugLabel(emoji)}`
|
||||
`Expected a valid emoji variant value, got ${Emoji.getDebugLabel(emoji)}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const emojiVariantKey = getEmojiVariantKeyByValue(emoji);
|
||||
const emojiVariant = getEmojiVariantByKey(emojiVariantKey);
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
@ -58,7 +49,7 @@ export const ReactionPickerPickerEmojiButton = forwardRef<
|
||||
role="img"
|
||||
aria-label={title ?? ''}
|
||||
size={48}
|
||||
emoji={emojiVariant}
|
||||
emoji={emoji}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
getDefaultGroup,
|
||||
} from '../test-helpers/getDefaultConversation.std.ts';
|
||||
import { getFakeDistributionListsWithMembers } from '../test-helpers/getFakeDistributionLists.std.ts';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -45,7 +45,7 @@ export default {
|
||||
'setMyStoriesToAllSignalConnections'
|
||||
),
|
||||
signalConnections: Array.from(Array(42), getDefaultConversation),
|
||||
emojiSkinToneDefault: EmojiSkinTone.None,
|
||||
emojiSkinToneDefault: Emoji.SkinTone.None,
|
||||
toggleSignalConnectionsModal: action('toggleSignalConnectionsModal'),
|
||||
},
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
@ -14,8 +14,7 @@ import { VIDEO_MP4 } from '../types/MIME.std.ts';
|
||||
import { fakeAttachment } from '../test-helpers/fakeAttachment.std.ts';
|
||||
import { getDefaultConversation } from '../test-helpers/getDefaultConversation.std.ts';
|
||||
import { getFakeStoryView } from '../test-helpers/getFakeStory.dom.tsx';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../reactions/constants.std.ts';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -48,11 +47,13 @@ export default {
|
||||
onTextTooLong: action('onTextTooLong'),
|
||||
onSelectEmoji: action('onSelectEmoji'),
|
||||
onMediaPlaybackStart: action('onMediaPlaybackStart'),
|
||||
preferredReactionEmoji: DEFAULT_PREFERRED_REACTION_EMOJI,
|
||||
preferredReactionEmoji: Emoji.getDefaultPreferredReactionEmojis(
|
||||
Emoji.SkinTone.None
|
||||
),
|
||||
queueStoryDownload: action('queueStoryDownload'),
|
||||
retryMessageSend: action('retryMessageSend'),
|
||||
showToast: action('showToast'),
|
||||
emojiSkinToneDefault: EmojiSkinTone.None,
|
||||
emojiSkinToneDefault: Emoji.SkinTone.None,
|
||||
story: getFakeStoryView(),
|
||||
storyViewMode: StoryViewModeType.All,
|
||||
viewStory: action('viewStory'),
|
||||
|
||||
@ -51,9 +51,9 @@ import { MessageBody } from './conversation/MessageBody.dom.tsx';
|
||||
import { RenderLocation } from './conversation/MessageTextRenderer.dom.tsx';
|
||||
import { arrow } from '../util/keyboard.dom.ts';
|
||||
import { StoryProgressSegment } from './StoryProgressSegment.dom.tsx';
|
||||
import type { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.dom.tsx';
|
||||
import type { ContactModalStateType } from '../types/globalModals.std.ts';
|
||||
import type { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const log = createLogger('StoryViewer');
|
||||
|
||||
@ -95,7 +95,7 @@ export type PropsType = {
|
||||
onGoToConversation: (conversationId: string) => unknown;
|
||||
onHideStory: (conversationId: string) => unknown;
|
||||
onTextTooLong: () => unknown;
|
||||
onReactToStory: (emoji: string, story: StoryViewType) => unknown;
|
||||
onReactToStory: (emoji: Emoji.Variant, story: StoryViewType) => unknown;
|
||||
onReplyToStory: (
|
||||
message: string,
|
||||
bodyRanges: DraftBodyRanges,
|
||||
@ -106,7 +106,7 @@ export type PropsType = {
|
||||
onMediaPlaybackStart: () => void;
|
||||
ourConversationId: string | undefined;
|
||||
platform: string;
|
||||
preferredReactionEmoji: ReadonlyArray<string>;
|
||||
preferredReactionEmoji: ReadonlyArray<Emoji.Variant>;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
replyState?: ReplyStateType;
|
||||
retryMessageSend: (messageId: string) => unknown;
|
||||
@ -114,7 +114,7 @@ export type PropsType = {
|
||||
setHasAllStoriesUnmuted: (isUnmuted: boolean) => unknown;
|
||||
showContactModal: (payload: ContactModalStateType) => void;
|
||||
showToast: ShowToastAction;
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
emojiSkinToneDefault: Emoji.SkinTone | null;
|
||||
story: StoryViewType;
|
||||
storyViewMode: StoryViewModeType;
|
||||
viewStory: ViewStoryActionCreatorType;
|
||||
@ -178,7 +178,9 @@ export function StoryViewer({
|
||||
useState<boolean>(false);
|
||||
const [storyDuration, setStoryDuration] = useState<number | undefined>();
|
||||
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
|
||||
const [reactionEmoji, setReactionEmoji] = useState<string | undefined>();
|
||||
const [reactionEmoji, setReactionEmoji] = useState<
|
||||
Emoji.Variant | undefined
|
||||
>();
|
||||
const [confirmDeleteStory, setConfirmDeleteStory] = useState<
|
||||
StoryViewType | undefined
|
||||
>();
|
||||
|
||||
@ -12,7 +12,7 @@ import { SendStatus } from '../messages/MessageSendState.std.ts';
|
||||
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal.dom.tsx';
|
||||
import { getDefaultConversation } from '../test-helpers/getDefaultConversation.std.ts';
|
||||
import { StoryViewTargetType } from '../types/Stories.std.ts';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../reactions/constants.std.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -40,7 +40,9 @@ export default {
|
||||
onReply: action('onReply'),
|
||||
onTextTooLong: action('onTextTooLong'),
|
||||
onSelectEmoji: action('onSelectEmoji'),
|
||||
preferredReactionEmoji: DEFAULT_PREFERRED_REACTION_EMOJI,
|
||||
preferredReactionEmoji: Emoji.getDefaultPreferredReactionEmojis(
|
||||
Emoji.SkinTone.None
|
||||
),
|
||||
replies: [],
|
||||
views: [],
|
||||
viewTarget: StoryViewTargetType.Views,
|
||||
@ -119,7 +121,7 @@ function getViewsAndReplies() {
|
||||
author: p4,
|
||||
conversationId: p4.id,
|
||||
id: generateUuid(),
|
||||
reactionEmoji: '❤️',
|
||||
reactionEmoji: Emoji.HEART,
|
||||
timestamp: Date.now() - 5 * durations.MINUTE,
|
||||
},
|
||||
{
|
||||
|
||||
@ -31,7 +31,6 @@ import { WidthBreakpoint } from './_util.std.ts';
|
||||
import { getAvatarColor } from '../types/Colors.std.ts';
|
||||
import { shouldNeverBeCalled } from '../util/shouldNeverBeCalled.std.ts';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog.dom.tsx';
|
||||
import type { EmojiSkinTone } from './fun/data/emojis.std.ts';
|
||||
import { FunEmojiPicker } from './fun/FunEmojiPicker.dom.tsx';
|
||||
import { FunEmojiPickerButton } from './fun/FunButton.dom.tsx';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.dom.tsx';
|
||||
@ -40,6 +39,7 @@ import { AxoContextMenu } from '../axo/AxoContextMenu.dom.tsx';
|
||||
import type { AxoMenuBuilder } from '../axo/AxoMenuBuilder.dom.tsx';
|
||||
import { drop } from '../util/drop.std.ts';
|
||||
import type { ContactModalStateType } from '../types/globalModals.std.ts';
|
||||
import type { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { noop, orderBy } = lodash;
|
||||
|
||||
@ -111,7 +111,7 @@ export type PropsType = {
|
||||
isInternalUser?: boolean;
|
||||
onChangeViewTarget: (target: StoryViewTargetType) => unknown;
|
||||
onClose: () => unknown;
|
||||
onReact: (emoji: string) => unknown;
|
||||
onReact: (emoji: Emoji.Variant) => unknown;
|
||||
onReply: (
|
||||
message: string,
|
||||
bodyRanges: DraftBodyRanges,
|
||||
@ -120,10 +120,10 @@ export type PropsType = {
|
||||
onTextTooLong: () => unknown;
|
||||
onSelectEmoji: (emojiSelection: FunEmojiSelection) => unknown;
|
||||
ourConversationId: string | undefined;
|
||||
preferredReactionEmoji: ReadonlyArray<string>;
|
||||
preferredReactionEmoji: ReadonlyArray<Emoji.Variant>;
|
||||
replies: ReadonlyArray<ReplyType>;
|
||||
showContactModal: (payload: ContactModalStateType) => void;
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
emojiSkinToneDefault: Emoji.SkinTone | null;
|
||||
sortedGroupMembers?: ReadonlyArray<ConversationType>;
|
||||
views: ReadonlyArray<StorySendStateType>;
|
||||
viewTarget: StoryViewTargetType;
|
||||
|
||||
@ -28,7 +28,6 @@ import { handleOutsideClick } from '../util/handleOutsideClick.dom.ts';
|
||||
import { Spinner } from './Spinner.dom.tsx';
|
||||
import { FunEmojiPicker } from './fun/FunEmojiPicker.dom.tsx';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.dom.tsx';
|
||||
import { getEmojiVariantByKey } from './fun/data/emojis.std.ts';
|
||||
import { FunEmojiPickerButton } from './fun/FunButton.dom.tsx';
|
||||
import { useConfirmDiscard } from '../hooks/useConfirmDiscard.dom.tsx';
|
||||
|
||||
@ -341,8 +340,7 @@ export function TextStoryCreator({
|
||||
|
||||
const handleSelectEmoji = useCallback(
|
||||
(emojiSelection: FunEmojiSelection) => {
|
||||
const emojiVariant = getEmojiVariantByKey(emojiSelection.variantKey);
|
||||
const emojiValue = emojiVariant.value;
|
||||
const { emoji } = emojiSelection;
|
||||
|
||||
onSelectEmoji(emojiSelection);
|
||||
|
||||
@ -353,7 +351,7 @@ export function TextStoryCreator({
|
||||
const before = originalText.substr(0, insertAt);
|
||||
const after = originalText.substr(insertAt, originalText.length);
|
||||
|
||||
return `${before}${emojiValue}${after}`;
|
||||
return `${before}${emoji}${after}`;
|
||||
});
|
||||
},
|
||||
[onSelectEmoji]
|
||||
|
||||
@ -8,6 +8,7 @@ import type { PropsType } from './AboutContactModal.dom.tsx';
|
||||
import { AboutContactModal } from './AboutContactModal.dom.tsx';
|
||||
import { type ComponentMeta } from '../../storybook/types.std.ts';
|
||||
import { getDefaultConversation } from '../../test-helpers/getDefaultConversation.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -107,7 +108,7 @@ export function MeWithLabel(args: PropsType): JSX.Element {
|
||||
<AboutContactModal
|
||||
{...{
|
||||
...args,
|
||||
contactLabelEmoji: '🐝',
|
||||
contactLabelEmoji: Emoji.BEE,
|
||||
contactLabelString: 'Worker Bee',
|
||||
contactNameColor: '270',
|
||||
}}
|
||||
@ -121,7 +122,7 @@ export function LongLabel(args: PropsType): JSX.Element {
|
||||
<AboutContactModal
|
||||
{...{
|
||||
...args,
|
||||
contactLabelEmoji: '🐝',
|
||||
contactLabelEmoji: Emoji.BEE,
|
||||
contactLabelString: '𒐫 𒐫 𒐫 𒐫 𒐫 𒐫 𒐫 𒐫 𒐫 𒐫 𒐫 𒐫 𒐫',
|
||||
contactNameColor: '270',
|
||||
}}
|
||||
@ -135,7 +136,7 @@ export function LongLabelAllEmoji(args: PropsType): JSX.Element {
|
||||
<AboutContactModal
|
||||
{...{
|
||||
...args,
|
||||
contactLabelEmoji: '🐝',
|
||||
contactLabelEmoji: Emoji.BEE,
|
||||
contactLabelString: '🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝',
|
||||
contactNameColor: '270',
|
||||
}}
|
||||
@ -149,7 +150,7 @@ export function MeWithInvalidLabelEmoji(args: PropsType): JSX.Element {
|
||||
<AboutContactModal
|
||||
{...{
|
||||
...args,
|
||||
contactLabelEmoji: '@',
|
||||
contactLabelEmoji: Emoji.unsafeCastMaybeInvalidStringToVariant('@'),
|
||||
contactLabelString: 'Worker Bee',
|
||||
contactNameColor: '270',
|
||||
}}
|
||||
|
||||
@ -8,7 +8,6 @@ import {
|
||||
type JSX,
|
||||
type MouseEvent,
|
||||
} from 'react';
|
||||
|
||||
import { isInSystemContacts } from '../../util/isInSystemContacts.std.ts';
|
||||
import { Avatar, AvatarBlur, AvatarSize } from '../Avatar.dom.tsx';
|
||||
import { Modal } from '../Modal.dom.tsx';
|
||||
@ -18,17 +17,11 @@ import { About } from './About.dom.tsx';
|
||||
import { I18n } from '../I18n.dom.tsx';
|
||||
import { canHaveNicknameAndNote } from '../../util/nicknames.dom.ts';
|
||||
import { Tooltip, TooltipPlacement } from '../Tooltip.dom.tsx';
|
||||
import { useFunEmojiLocalizer } from '../fun/useFunEmojiLocalizer.dom.tsx';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from '../fun/data/emojis.std.ts';
|
||||
import { FunStaticEmoji } from '../fun/FunEmoji.dom.tsx';
|
||||
import { missingEmojiPlaceholder } from '../../types/GroupMemberLabels.std.ts';
|
||||
|
||||
import type { ConversationType } from '../../state/ducks/conversations.preload.ts';
|
||||
import type { LocalizerType } from '../../types/Util.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
function muted(parts: Array<string | JSX.Element>) {
|
||||
return (
|
||||
@ -40,7 +33,7 @@ export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
canAddLabel: boolean;
|
||||
contact: ConversationType;
|
||||
contactLabelEmoji: string | undefined;
|
||||
contactLabelEmoji: Emoji.Variant | undefined;
|
||||
contactLabelString: string | undefined;
|
||||
contactNameColor: string | undefined;
|
||||
fromOrAddedByTrustedContact?: boolean;
|
||||
@ -135,23 +128,20 @@ export function AboutContactModal({
|
||||
const shouldShowLabel = isMe && hasLabel;
|
||||
const shouldShowAddLabel =
|
||||
isMe && !hasLabel && canAddLabel && isEditMemberLabelEnabled;
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
|
||||
let labelEmojiElement;
|
||||
if (
|
||||
shouldShowLabel &&
|
||||
contactLabelEmoji &&
|
||||
isEmojiVariantValue(contactLabelEmoji)
|
||||
Emoji.isEmoji(contactLabelEmoji)
|
||||
) {
|
||||
const emojiKey = getEmojiVariantKeyByValue(contactLabelEmoji);
|
||||
const labelEmojiData = getEmojiVariantByKey(emojiKey);
|
||||
labelEmojiElement = (
|
||||
<>
|
||||
<FunStaticEmoji
|
||||
role="img"
|
||||
aria-label={emojiLocalizer.getLocaleShortName(labelEmojiData.key)}
|
||||
aria-label={Emoji.getDisplayLabel(contactLabelEmoji)}
|
||||
size={14}
|
||||
emoji={labelEmojiData}
|
||||
emoji={contactLabelEmoji}
|
||||
/>{' '}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -12,6 +12,7 @@ import { ThemeType } from '../../types/Util.std.ts';
|
||||
import { getDefaultConversation } from '../../test-helpers/getDefaultConversation.std.ts';
|
||||
import { getFakeBadges } from '../../test-helpers/getFakeBadge.std.ts';
|
||||
import { SignalService as Proto } from '../../protobuf/index.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||
|
||||
@ -85,7 +86,7 @@ AsNonAdmin.args = {
|
||||
export const WithLabel = Template.bind({});
|
||||
WithLabel.args = {
|
||||
areWeAdmin: false,
|
||||
contactLabelEmoji: '💪🏼',
|
||||
contactLabelEmoji: Emoji.getVariant(Emoji.MUSCLE, Emoji.SkinTone.Type2),
|
||||
contactLabelString: 'Strong',
|
||||
contactNameColor: '180',
|
||||
};
|
||||
@ -100,28 +101,28 @@ WithLabelNoEmoji.args = {
|
||||
export const WithLabelInvalidEmoji = Template.bind({});
|
||||
WithLabelInvalidEmoji.args = {
|
||||
areWeAdmin: false,
|
||||
contactLabelEmoji: '%',
|
||||
contactLabelEmoji: Emoji.unsafeCastMaybeInvalidStringToVariant('%'),
|
||||
contactLabelString: 'Strong',
|
||||
contactNameColor: '220',
|
||||
};
|
||||
|
||||
export const LongLabel = Template.bind({});
|
||||
LongLabel.args = {
|
||||
contactLabelEmoji: '🐝',
|
||||
contactLabelEmoji: Emoji.BEE,
|
||||
contactLabelString: '𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫',
|
||||
contactNameColor: '270',
|
||||
};
|
||||
|
||||
export const LongLabel2 = Template.bind({});
|
||||
LongLabel2.args = {
|
||||
contactLabelEmoji: '🐝',
|
||||
contactLabelEmoji: Emoji.BEE,
|
||||
contactLabelString: '﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽',
|
||||
contactNameColor: '270',
|
||||
};
|
||||
|
||||
export const LongLabelAllEmoji = Template.bind({});
|
||||
LongLabelAllEmoji.args = {
|
||||
contactLabelEmoji: '🐝',
|
||||
contactLabelEmoji: Emoji.BEE,
|
||||
contactLabelString: '🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝🐝',
|
||||
contactNameColor: '270',
|
||||
};
|
||||
|
||||
@ -40,6 +40,7 @@ import { tw } from '../../axo/tw.dom.tsx';
|
||||
import { strictAssert } from '../../util/assert.std.ts';
|
||||
import type { RemoveClientType } from '../../types/Calling.std.ts';
|
||||
import type { ContactNameColorType } from '../../types/Colors.std.ts';
|
||||
import type { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||
|
||||
@ -51,7 +52,7 @@ export type PropsDataType = {
|
||||
areWeAdmin: boolean;
|
||||
badges: ReadonlyArray<BadgeType>;
|
||||
contact?: ConversationType;
|
||||
contactLabelEmoji: string | undefined;
|
||||
contactLabelEmoji: Emoji.Variant | undefined;
|
||||
contactLabelString: string | undefined;
|
||||
contactNameColor: ContactNameColorType | undefined;
|
||||
conversation?: ConversationType;
|
||||
|
||||
@ -7,6 +7,7 @@ import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './ContactName.dom.tsx';
|
||||
import { ContactName } from './ContactName.dom.tsx';
|
||||
import { ContactNameColors } from '../../types/Colors.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/ContactName',
|
||||
@ -32,7 +33,7 @@ export function WithLongLabel(): JSX.Element {
|
||||
<ContactName
|
||||
title="Troublemaker"
|
||||
contactLabel={{
|
||||
labelEmoji: '✅',
|
||||
labelEmoji: Emoji.CHECKMARK,
|
||||
labelString:
|
||||
"this is a long label. really long. why don't we see what happens?",
|
||||
}}
|
||||
@ -48,7 +49,7 @@ export function WithLabelWithBigUnicode(): JSX.Element {
|
||||
<ContactName
|
||||
title="Troublemaker"
|
||||
contactLabel={{
|
||||
labelEmoji: '✅',
|
||||
labelEmoji: Emoji.CHECKMARK,
|
||||
labelString: '𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫𒐫',
|
||||
}}
|
||||
contactNameColor="140"
|
||||
@ -77,7 +78,10 @@ export function ColorsWithLabels(): JSX.Element {
|
||||
<ContactName
|
||||
title={`Hello ${color}`}
|
||||
contactNameColor={color}
|
||||
contactLabel={{ labelEmoji: '✅', labelString: 'Task Wrangler' }}
|
||||
contactLabel={{
|
||||
labelEmoji: Emoji.CHECKMARK,
|
||||
labelString: 'Task Wrangler',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@ -113,7 +117,7 @@ export function ColorsWithInvalidLabelEmoji(): JSX.Element {
|
||||
title={`Hello ${color}`}
|
||||
contactNameColor={color}
|
||||
contactLabel={{
|
||||
labelEmoji: '&',
|
||||
labelEmoji: Emoji.unsafeCastMaybeInvalidStringToVariant('&'),
|
||||
labelString: 'Task Wrangler',
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -8,12 +8,6 @@ import type { ReactNode, JSX, MouseEvent } from 'react';
|
||||
|
||||
import { getClassNamesFor } from '../../util/getClassNamesFor.std.ts';
|
||||
import { isSignalConversation as getIsSignalConversation } from '../../util/isSignalConversation.dom.ts';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from '../fun/data/emojis.std.ts';
|
||||
import { useFunEmojiLocalizer } from '../fun/useFunEmojiLocalizer.dom.tsx';
|
||||
import { FunStaticEmoji } from '../fun/FunEmoji.dom.tsx';
|
||||
import { missingEmojiPlaceholder } from '../../types/GroupMemberLabels.std.ts';
|
||||
|
||||
@ -23,10 +17,11 @@ import type { ContactNameColorType } from '../../types/Colors.std.ts';
|
||||
import type { FunStaticEmojiSize } from '../fun/FunEmoji.dom.tsx';
|
||||
import { UserText } from '../UserText.dom.tsx';
|
||||
import { OfficialChatInlineBadge } from './OfficialChatInlineBadge.dom.tsx';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
export type ContactNameData = {
|
||||
contactNameColor?: ContactNameColorType;
|
||||
contactLabel?: { labelString: string; labelEmoji: string | undefined };
|
||||
contactLabel?: { labelString: string; labelEmoji: Emoji.Variant | undefined };
|
||||
firstName?: string;
|
||||
isSignalConversation?: boolean;
|
||||
isMe?: boolean;
|
||||
@ -134,7 +129,6 @@ export function GroupMemberLabel({
|
||||
context: Context;
|
||||
module?: string;
|
||||
}): ReactNode {
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
const getClassName = getClassNamesFor('module-contact-name', module);
|
||||
|
||||
if (!contactLabel) {
|
||||
@ -144,10 +138,7 @@ export function GroupMemberLabel({
|
||||
const { labelEmoji, labelString } = contactLabel;
|
||||
|
||||
let emojiElement;
|
||||
if (labelEmoji && isEmojiVariantValue(labelEmoji)) {
|
||||
const emojiKey = getEmojiVariantKeyByValue(labelEmoji);
|
||||
const emojiData = getEmojiVariantByKey(emojiKey);
|
||||
|
||||
if (labelEmoji && Emoji.isEmoji(labelEmoji)) {
|
||||
emojiElement = (
|
||||
<span
|
||||
className={classNames(
|
||||
@ -157,9 +148,9 @@ export function GroupMemberLabel({
|
||||
>
|
||||
<FunStaticEmoji
|
||||
role="img"
|
||||
aria-label={emojiLocalizer.getLocaleShortName(emojiData.key)}
|
||||
aria-label={Emoji.getDisplayLabel(labelEmoji)}
|
||||
size={emojiSize}
|
||||
emoji={emojiData}
|
||||
emoji={Emoji.ignorePreferredSkinTone(labelEmoji)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
@ -12,6 +12,7 @@ import { StorybookThemeContext } from '../../../.storybook/StorybookThemeContext
|
||||
import { getDefaultConversation } from '../../test-helpers/getDefaultConversation.std.ts';
|
||||
import { ThemeType } from '../../types/Util.std.ts';
|
||||
import type { GroupV2Membership } from './conversation-details/ConversationDetailsMembershipList.dom.tsx';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -29,7 +30,7 @@ const createMemberships = ({
|
||||
return Array.from(new Array(count)).map(
|
||||
(_, i): GroupV2Membership => ({
|
||||
isAdmin: i % 3 === 0,
|
||||
labelEmoji: i % 6 === 0 ? '🟢' : undefined,
|
||||
labelEmoji: i % 6 === 0 ? Emoji.GREEN_CIRCLE : undefined,
|
||||
labelString: i % 3 === 0 ? `Task Wrangler ${i}` : undefined,
|
||||
member: unknownContactIndices.includes(i)
|
||||
? getDefaultConversation({
|
||||
|
||||
@ -1,20 +1,11 @@
|
||||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { CSSProperties, JSX } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { RenderTextCallbackType } from '../../types/Util.std.ts';
|
||||
import { splitByEmoji } from '../../util/emoji.std.ts';
|
||||
import { missingCaseError } from '../../util/missingCaseError.std.ts';
|
||||
import { FunInlineEmoji } from '../fun/FunEmoji.dom.tsx';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
isEmojiVariantValueNonQualified,
|
||||
} from '../fun/data/emojis.std.ts';
|
||||
import { createLogger } from '../../logging/log.std.ts';
|
||||
import { useFunEmojiLocalizer } from '../fun/useFunEmojiLocalizer.dom.tsx';
|
||||
|
||||
const log = createLogger('Emojify');
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
export type Props = {
|
||||
fontSizeOverride?: number | null;
|
||||
@ -34,43 +25,31 @@ export function Emojify({
|
||||
renderNonEmoji = defaultRenderNonEmoji,
|
||||
style,
|
||||
}: Props): JSX.Element {
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
const segments = useMemo(() => {
|
||||
return Array.from(Emoji.getSegments(text));
|
||||
}, [text]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{splitByEmoji(text).map(({ type, value: match }, index) => {
|
||||
if (type === 'emoji') {
|
||||
// If we don't recognize the emoji, render it as text.
|
||||
if (!isEmojiVariantValue(match)) {
|
||||
log.warn('Found emoji that we did not recognize', match.length);
|
||||
return renderNonEmoji({ text: match, key: index });
|
||||
}
|
||||
|
||||
// Render emoji as text if they are a non-qualified emoji value.
|
||||
if (isEmojiVariantValueNonQualified(match)) {
|
||||
return renderNonEmoji({ text: match, key: index });
|
||||
}
|
||||
|
||||
const variantKey = getEmojiVariantKeyByValue(match);
|
||||
const variant = getEmojiVariantByKey(variantKey);
|
||||
|
||||
{segments.map(segment => {
|
||||
if (segment.kind === 'emoji') {
|
||||
return (
|
||||
<FunInlineEmoji
|
||||
// oxlint-disable-next-line react/no-array-index-key
|
||||
key={index}
|
||||
key={segment.offset}
|
||||
role="img"
|
||||
aria-label={emojiLocalizer.getLocaleShortName(variantKey)}
|
||||
emoji={variant}
|
||||
aria-label={Emoji.getDisplayLabel(segment.value)}
|
||||
emoji={segment.value}
|
||||
size={fontSizeOverride}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'text') {
|
||||
return renderNonEmoji({ text: match, key: index });
|
||||
if (segment.kind === 'text') {
|
||||
return renderNonEmoji({ text: segment.value, key: segment.offset });
|
||||
}
|
||||
|
||||
throw missingCaseError(type);
|
||||
throw missingCaseError(segment);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -10,8 +10,8 @@ import {
|
||||
isLinkSneaky,
|
||||
shouldLinkifyMessage,
|
||||
} from '../../types/LinkPreview.std.ts';
|
||||
import { splitByEmoji } from '../../util/emoji.std.ts';
|
||||
import { missingCaseError } from '../../util/missingCaseError.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
export const linkify = new LinkifyIt()
|
||||
// This is all TLDs in place in 2010, according to [IANA's root zone database][0]
|
||||
@ -326,6 +326,11 @@ export const SUPPORTED_PROTOCOLS = /^(http|https):/i;
|
||||
|
||||
const defaultRenderNonLink: RenderTextCallbackType = ({ text }) => text;
|
||||
|
||||
type ChunkDataItem = Readonly<{
|
||||
chunk: string;
|
||||
matchData: ReadonlyArray<LinkifyItMatch>;
|
||||
}>;
|
||||
|
||||
export function Linkify(props: Props): JSX.Element {
|
||||
const { text, renderNonLink = defaultRenderNonLink } = props;
|
||||
|
||||
@ -333,19 +338,20 @@ export function Linkify(props: Props): JSX.Element {
|
||||
return <>{renderNonLink({ text, key: 1 })}</>;
|
||||
}
|
||||
|
||||
const chunkData: Array<{
|
||||
chunk: string;
|
||||
matchData: ReadonlyArray<LinkifyItMatch>;
|
||||
}> = splitByEmoji(text).map(({ type, value: chunk }) => {
|
||||
if (type === 'text') {
|
||||
const segments = Array.from(Emoji.getSegments(text));
|
||||
|
||||
const chunkData = segments.map((segment): ChunkDataItem => {
|
||||
const chunk = segment.value;
|
||||
|
||||
if (segment.kind === 'text') {
|
||||
return { chunk, matchData: linkify.match(chunk) || [] };
|
||||
}
|
||||
|
||||
if (type === 'emoji') {
|
||||
if (segment.kind === 'emoji') {
|
||||
return { chunk, matchData: [] };
|
||||
}
|
||||
|
||||
throw missingCaseError(type);
|
||||
throw missingCaseError(segment);
|
||||
});
|
||||
|
||||
const results: Array<JSX.Element | string> = [];
|
||||
|
||||
@ -116,23 +116,14 @@ import { TapToViewNotAvailableType } from '../TapToViewNotAvailableModal.dom.tsx
|
||||
import type { DataPropsType as TapToViewNotAvailablePropsType } from '../TapToViewNotAvailableModal.dom.tsx';
|
||||
import { FileThumbnail } from '../FileThumbnail.dom.tsx';
|
||||
import { FunStaticEmoji } from '../fun/FunEmoji.dom.tsx';
|
||||
import {
|
||||
type EmojifyData,
|
||||
getEmojiDebugLabel,
|
||||
getEmojifyData,
|
||||
getEmojiParentByKey,
|
||||
getEmojiParentKeyByVariantKey,
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from '../fun/data/emojis.std.ts';
|
||||
import { useGroupedAndOrderedReactions } from '../../util/groupAndOrderReactions.dom.ts';
|
||||
import { useGroupedAndOrderedReactions } from '../../util/groupAndOrderReactions.std.ts';
|
||||
import type { AxoMenuBuilder } from '../../axo/AxoMenuBuilder.dom.tsx';
|
||||
import { AxoSymbol } from '../../axo/AxoSymbol.dom.tsx';
|
||||
import type { RenderAudioAttachmentProps } from '../../state/smart/renderAudioAttachment.preload.tsx';
|
||||
import type { MemberLabelType } from '../../types/GroupMemberLabels.std.ts';
|
||||
import type { ContactModalStateType } from '../../types/globalModals.std.ts';
|
||||
import { tw } from '../../axo/tw.dom.tsx';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { drop, take, unescape } = lodash;
|
||||
|
||||
@ -202,25 +193,18 @@ export type GiftBadgeType =
|
||||
state: GiftBadgeStates.Failed;
|
||||
};
|
||||
|
||||
function ReactionEmoji(props: { emojiVariantValue: string }) {
|
||||
if (!isEmojiVariantValue(props.emojiVariantValue)) {
|
||||
log.error(
|
||||
`Expected a valid emoji variant value, got ${getEmojiDebugLabel(props.emojiVariantValue)}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const emojiVariantKey = getEmojiVariantKeyByValue(props.emojiVariantValue);
|
||||
const emojiVariant = getEmojiVariantByKey(emojiVariantKey);
|
||||
const emojiParentKey = getEmojiParentKeyByVariantKey(emojiVariantKey);
|
||||
const emojiParent = getEmojiParentByKey(emojiParentKey);
|
||||
type EmojifyData = Readonly<{
|
||||
text: string;
|
||||
count: Emoji.JumboEmojiCount;
|
||||
}>;
|
||||
|
||||
function ReactionEmoji(props: { emoji: Emoji.Variant }) {
|
||||
return (
|
||||
<FunStaticEmoji
|
||||
role="img"
|
||||
aria-label={emojiParent.englishShortNameDefault}
|
||||
aria-label={Emoji.getDisplayLabel(props.emoji)}
|
||||
size={16}
|
||||
emoji={emojiVariant}
|
||||
emoji={props.emoji}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -300,7 +284,7 @@ export type PropsData = {
|
||||
authorTitle: string;
|
||||
conversationColor: ConversationColorType;
|
||||
customColor?: CustomColorType;
|
||||
emoji?: string;
|
||||
emoji?: Emoji.Variant;
|
||||
isFromMe: boolean;
|
||||
rawAttachment?: QuotedAttachmentForUIType;
|
||||
storyId?: string;
|
||||
@ -465,7 +449,7 @@ const MessageReactions = forwardRef(function MessageReactions(
|
||||
}: MessageReactionsProps,
|
||||
parentRef
|
||||
): JSX.Element {
|
||||
const ordered = useGroupedAndOrderedReactions(reactions, 'parentKey');
|
||||
const ordered = useGroupedAndOrderedReactions(reactions, 'parent');
|
||||
|
||||
const reactionsContainerRefMerger = useRef(createRefMerger());
|
||||
|
||||
@ -578,7 +562,7 @@ const MessageReactions = forwardRef(function MessageReactions(
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<ReactionEmoji emojiVariantValue={re.emoji} />
|
||||
<ReactionEmoji emoji={re.emoji} />
|
||||
{re.count > 1 ? (
|
||||
<span
|
||||
className={classNames(
|
||||
@ -1027,15 +1011,14 @@ export class Message extends PureComponent<Props, State> {
|
||||
this.#cachedEmojifyData == null ||
|
||||
this.#cachedEmojifyData.text !== text
|
||||
) {
|
||||
this.#cachedEmojifyData = getEmojifyData(text);
|
||||
this.#cachedEmojifyData = {
|
||||
text,
|
||||
count: Emoji.getJumboEmojiCount(text),
|
||||
};
|
||||
}
|
||||
const emojifyData = this.#cachedEmojifyData;
|
||||
|
||||
if (
|
||||
!emojifyData.isEmojiOnlyText ||
|
||||
emojifyData.emojiCount === 0 ||
|
||||
emojifyData.emojiCount >= 6
|
||||
) {
|
||||
if (emojifyData.count == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -12,30 +12,30 @@ import type { RenderLocation } from './MessageTextRenderer.dom.tsx';
|
||||
import { UserText } from '../UserText.dom.tsx';
|
||||
import { shouldLinkifyMessage } from '../../types/LinkPreview.std.ts';
|
||||
import { FunJumboEmojiSize } from '../fun/FunEmoji.dom.tsx';
|
||||
import { getEmojifyData } from '../fun/data/emojis.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
import { missingCaseError } from '../../util/missingCaseError.std.ts';
|
||||
|
||||
function getSizeClass(str: string): FunJumboEmojiSize | null {
|
||||
const emojifyData = getEmojifyData(str);
|
||||
// Do we have non-emoji characters?
|
||||
if (!emojifyData.isEmojiOnlyText) {
|
||||
const count = Emoji.getJumboEmojiCount(str);
|
||||
if (count == null) {
|
||||
return null;
|
||||
}
|
||||
if (emojifyData.emojiCount === 1) {
|
||||
if (count === 1) {
|
||||
return FunJumboEmojiSize.Max;
|
||||
}
|
||||
if (emojifyData.emojiCount === 2) {
|
||||
if (count === 2) {
|
||||
return FunJumboEmojiSize.ExtraLarge;
|
||||
}
|
||||
if (emojifyData.emojiCount === 3) {
|
||||
if (count === 3) {
|
||||
return FunJumboEmojiSize.Large;
|
||||
}
|
||||
if (emojifyData.emojiCount === 4) {
|
||||
if (count === 4) {
|
||||
return FunJumboEmojiSize.Medium;
|
||||
}
|
||||
if (emojifyData.emojiCount === 5) {
|
||||
if (count === 5) {
|
||||
return FunJumboEmojiSize.Small;
|
||||
}
|
||||
return null;
|
||||
throw missingCaseError(count);
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { ReactElement, JSX } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
import lodash from 'lodash';
|
||||
|
||||
import { linkify, SUPPORTED_PROTOCOLS } from './Linkify.dom.tsx';
|
||||
@ -26,10 +25,10 @@ import { Emojify } from './Emojify.dom.tsx';
|
||||
import { AddNewLines } from './AddNewLines.dom.tsx';
|
||||
import type { LocalizerType } from '../../types/Util.std.ts';
|
||||
import type { FunJumboEmojiSize } from '../fun/FunEmoji.dom.tsx';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { sortBy } = lodash;
|
||||
|
||||
const EMOJI_REGEXP = emojiRegex();
|
||||
export enum RenderLocation {
|
||||
ConversationList = 'ConversationList',
|
||||
Quote = 'Quote',
|
||||
@ -444,7 +443,7 @@ function extractLinks(
|
||||
// to support emojis immediately before links
|
||||
// we replace emojis with a space for each byte
|
||||
const matches = linkify.match(
|
||||
originalMessageText.replace(EMOJI_REGEXP, s => ' '.repeat(s.length))
|
||||
Emoji.replaceEmojiWithSpaces(originalMessageText)
|
||||
);
|
||||
|
||||
if (matches == null) {
|
||||
|
||||
@ -26,6 +26,7 @@ import { getDefaultConversation } from '../../test-helpers/getDefaultConversatio
|
||||
import { WidthBreakpoint } from '../_util.std.ts';
|
||||
import { ThemeType } from '../../types/Util.std.ts';
|
||||
import { PaymentEventKind } from '../../types/Payment.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -232,7 +233,7 @@ IncomingByAnotherWithLabel.args = {
|
||||
authorTitle: getDefaultConversation().title,
|
||||
isIncoming: true,
|
||||
authorLabel: {
|
||||
labelEmoji: '1️⃣',
|
||||
labelEmoji: Emoji.ONE,
|
||||
labelString: 'First',
|
||||
},
|
||||
};
|
||||
@ -251,7 +252,7 @@ export function IncomingOutgoingColors(args: Props): JSX.Element {
|
||||
...args,
|
||||
conversationColor: color,
|
||||
authorLabel: {
|
||||
labelEmoji: '1️⃣',
|
||||
labelEmoji: Emoji.ONE,
|
||||
labelString: 'First',
|
||||
},
|
||||
})
|
||||
@ -630,7 +631,7 @@ IsStoryReplyEmoji.args = {
|
||||
url: pngUrl,
|
||||
},
|
||||
},
|
||||
reactionEmoji: '🏋️',
|
||||
reactionEmoji: Emoji.getDefaultVariant(Emoji.WEIGHT_LIFTER),
|
||||
};
|
||||
|
||||
export const Payment = Template.bind({});
|
||||
|
||||
@ -33,6 +33,7 @@ import type {
|
||||
import type { AnyPaymentEvent } from '../../types/Payment.std.ts';
|
||||
import type { QuotedAttachmentType } from '../../model-types.d.ts';
|
||||
import type { MemberLabelType } from '../../types/GroupMemberLabels.std.ts';
|
||||
import type { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { noop } = lodash;
|
||||
|
||||
@ -64,7 +65,7 @@ export type Props = {
|
||||
payment?: AnyPaymentEvent;
|
||||
isGiftBadge: boolean;
|
||||
isViewOnce: boolean;
|
||||
reactionEmoji?: string;
|
||||
reactionEmoji?: Emoji.Variant;
|
||||
referencedMessageNotFound: boolean;
|
||||
doubleCheckMissingQuoteReference?: () => unknown;
|
||||
};
|
||||
|
||||
@ -7,7 +7,7 @@ import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { Props as ReactionPickerProps } from './ReactionPicker.dom.tsx';
|
||||
import { ReactionPicker } from './ReactionPicker.dom.tsx';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../../reactions/constants.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -20,7 +20,9 @@ export function Base(): JSX.Element {
|
||||
<ReactionPicker
|
||||
i18n={i18n}
|
||||
onPick={action('onPick')}
|
||||
preferredReactionEmoji={DEFAULT_PREFERRED_REACTION_EMOJI}
|
||||
preferredReactionEmoji={Emoji.getDefaultPreferredReactionEmojis(
|
||||
Emoji.SkinTone.None
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -28,13 +30,23 @@ export function Base(): JSX.Element {
|
||||
export function SelectedReaction(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{['❤️', '👍', '👎', '😂', '😮', '😢', '😡'].map(e => (
|
||||
{[
|
||||
Emoji.HEART,
|
||||
Emoji.getDefaultVariant(Emoji.THUMBS_UP),
|
||||
Emoji.getDefaultVariant(Emoji.THUMBS_DOWN),
|
||||
Emoji.JOY,
|
||||
Emoji.OPEN_MOUTH,
|
||||
Emoji.CRY,
|
||||
Emoji.RAGE,
|
||||
].map(e => (
|
||||
<div key={e} style={{ height: '100px' }}>
|
||||
<ReactionPicker
|
||||
i18n={i18n}
|
||||
selected={e}
|
||||
onPick={action('onPick')}
|
||||
preferredReactionEmoji={DEFAULT_PREFERRED_REACTION_EMOJI}
|
||||
preferredReactionEmoji={Emoji.getDefaultPreferredReactionEmojis(
|
||||
Emoji.SkinTone.None
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -16,19 +16,18 @@ import {
|
||||
ReactionPickerPickerEmojiButton,
|
||||
ReactionPickerPickerStyle,
|
||||
} from '../ReactionPickerPicker.dom.tsx';
|
||||
import type { EmojiVariantKey } from '../fun/data/emojis.std.ts';
|
||||
import { getEmojiVariantByKey } from '../fun/data/emojis.std.ts';
|
||||
import { FunEmojiPicker } from '../fun/FunEmojiPicker.dom.tsx';
|
||||
import type { FunEmojiSelection } from '../fun/panels/FunPanelEmojis.dom.tsx';
|
||||
import type { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
export type OwnProps = {
|
||||
i18n: LocalizerType;
|
||||
selected?: string;
|
||||
selected?: Emoji.Variant;
|
||||
onClose?: () => unknown;
|
||||
onPick: (emoji: string) => unknown;
|
||||
preferredReactionEmoji: ReadonlyArray<string>;
|
||||
onPick: (emoji: Emoji.Variant) => unknown;
|
||||
preferredReactionEmoji: ReadonlyArray<Emoji.Variant>;
|
||||
theme?: ThemeType;
|
||||
messageEmojis?: ReadonlyArray<EmojiVariantKey>;
|
||||
messageEmojis?: ReadonlyArray<Emoji.Variant>;
|
||||
};
|
||||
|
||||
export type Props = OwnProps & Pick<HTMLProps<HTMLDivElement>, 'style'>;
|
||||
@ -70,8 +69,7 @@ export const ReactionPicker = forwardRef<HTMLDivElement, Props>(
|
||||
|
||||
const onSelectEmoji = useCallback(
|
||||
(emojiSelection: FunEmojiSelection) => {
|
||||
const variant = getEmojiVariantByKey(emojiSelection.variantKey);
|
||||
onPick(variant.value);
|
||||
onPick(emojiSelection.emoji);
|
||||
},
|
||||
[onPick]
|
||||
);
|
||||
|
||||
@ -9,6 +9,7 @@ import type { Props } from './ReactionViewer.dom.tsx';
|
||||
import { ReactionViewer } from './ReactionViewer.dom.tsx';
|
||||
import { getDefaultConversation } from '../../test-helpers/getDefaultConversation.std.ts';
|
||||
import { ThemeType } from '../../types/Util.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -30,7 +31,7 @@ export function AllReactions(): JSX.Element {
|
||||
const props = createProps({
|
||||
reactions: [
|
||||
{
|
||||
emoji: '❤️',
|
||||
emoji: Emoji.HEART,
|
||||
timestamp: 1,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552671',
|
||||
@ -40,7 +41,7 @@ export function AllReactions(): JSX.Element {
|
||||
}),
|
||||
},
|
||||
{
|
||||
emoji: '❤️',
|
||||
emoji: Emoji.HEART,
|
||||
timestamp: 2,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552672',
|
||||
@ -49,7 +50,7 @@ export function AllReactions(): JSX.Element {
|
||||
}),
|
||||
},
|
||||
{
|
||||
emoji: '❤️',
|
||||
emoji: Emoji.HEART,
|
||||
timestamp: 3,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552673',
|
||||
@ -58,7 +59,7 @@ export function AllReactions(): JSX.Element {
|
||||
}),
|
||||
},
|
||||
{
|
||||
emoji: '❤️',
|
||||
emoji: Emoji.HEART,
|
||||
timestamp: 4,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552674',
|
||||
@ -67,7 +68,7 @@ export function AllReactions(): JSX.Element {
|
||||
}),
|
||||
},
|
||||
{
|
||||
emoji: '👍',
|
||||
emoji: Emoji.getDefaultVariant(Emoji.THUMBS_UP),
|
||||
timestamp: 9,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552678',
|
||||
@ -77,7 +78,7 @@ export function AllReactions(): JSX.Element {
|
||||
}),
|
||||
},
|
||||
{
|
||||
emoji: '👎',
|
||||
emoji: Emoji.getDefaultVariant(Emoji.THUMBS_DOWN),
|
||||
timestamp: 10,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552673',
|
||||
@ -86,7 +87,7 @@ export function AllReactions(): JSX.Element {
|
||||
}),
|
||||
},
|
||||
{
|
||||
emoji: '😂',
|
||||
emoji: Emoji.JOY,
|
||||
timestamp: 11,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552674',
|
||||
@ -95,7 +96,7 @@ export function AllReactions(): JSX.Element {
|
||||
}),
|
||||
},
|
||||
{
|
||||
emoji: '😮',
|
||||
emoji: Emoji.OPEN_MOUTH,
|
||||
timestamp: 12,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552675',
|
||||
@ -104,7 +105,7 @@ export function AllReactions(): JSX.Element {
|
||||
}),
|
||||
},
|
||||
{
|
||||
emoji: '😢',
|
||||
emoji: Emoji.CRY,
|
||||
timestamp: 13,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552676',
|
||||
@ -113,7 +114,7 @@ export function AllReactions(): JSX.Element {
|
||||
}),
|
||||
},
|
||||
{
|
||||
emoji: '😡',
|
||||
emoji: Emoji.RAGE,
|
||||
timestamp: 14,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552676',
|
||||
@ -128,10 +129,10 @@ export function AllReactions(): JSX.Element {
|
||||
|
||||
export function PickedReaction(): JSX.Element {
|
||||
const props = createProps({
|
||||
pickedReaction: '❤️',
|
||||
pickedReaction: Emoji.HEART,
|
||||
reactions: [
|
||||
{
|
||||
emoji: '❤️',
|
||||
emoji: Emoji.HEART,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552671',
|
||||
name: 'Amelia Briggs',
|
||||
@ -141,7 +142,7 @@ export function PickedReaction(): JSX.Element {
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
emoji: '👍',
|
||||
emoji: Emoji.getDefaultVariant(Emoji.THUMBS_UP),
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552671',
|
||||
phoneNumber: '+14155552671',
|
||||
@ -157,10 +158,10 @@ export function PickedReaction(): JSX.Element {
|
||||
|
||||
export function PickedMissingReaction(): JSX.Element {
|
||||
const props = createProps({
|
||||
pickedReaction: '😡',
|
||||
pickedReaction: Emoji.RAGE,
|
||||
reactions: [
|
||||
{
|
||||
emoji: '❤️',
|
||||
emoji: Emoji.HEART,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552671',
|
||||
name: 'Amelia Briggs',
|
||||
@ -170,7 +171,7 @@ export function PickedMissingReaction(): JSX.Element {
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
emoji: '👍',
|
||||
emoji: Emoji.getDefaultVariant(Emoji.THUMBS_UP),
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552671',
|
||||
phoneNumber: '+14155552671',
|
||||
@ -184,18 +185,16 @@ export function PickedMissingReaction(): JSX.Element {
|
||||
return <ReactionViewer {...props} />;
|
||||
}
|
||||
|
||||
const skinTones = [
|
||||
'\u{1F3FB}',
|
||||
'\u{1F3FC}',
|
||||
'\u{1F3FD}',
|
||||
'\u{1F3FE}',
|
||||
'\u{1F3FF}',
|
||||
];
|
||||
const thumbsUpHands = skinTones.map(skinTone => `👍${skinTone}`);
|
||||
const okHands = skinTones.map(skinTone => `👌${skinTone}`).reverse();
|
||||
const thumbsUpHands = Emoji.SKIN_TONE_ORDER.map(skinTone => {
|
||||
return Emoji.getVariant(Emoji.THUMBS_UP, skinTone);
|
||||
});
|
||||
|
||||
const okHands = Emoji.SKIN_TONE_ORDER.map(skinTone => {
|
||||
return Emoji.getVariant(Emoji.OK_HAND, skinTone);
|
||||
}).toReversed();
|
||||
|
||||
const createReaction = (
|
||||
emoji: string,
|
||||
emoji: Emoji.Variant,
|
||||
name: string,
|
||||
timestamp = Date.now()
|
||||
) => ({
|
||||
@ -210,7 +209,7 @@ const createReaction = (
|
||||
|
||||
export function ReactionSkinTones(): JSX.Element {
|
||||
const props = createProps({
|
||||
pickedReaction: '😡',
|
||||
pickedReaction: Emoji.RAGE,
|
||||
reactions: [
|
||||
...thumbsUpHands.map((emoji, n) =>
|
||||
createReaction(emoji, `Thumbs Up ${n + 1}`, Date.now() + n * 1000)
|
||||
|
||||
@ -19,29 +19,14 @@ import type { ConversationType } from '../../state/ducks/conversations.preload.t
|
||||
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges.preload.ts';
|
||||
import { useEscapeHandling } from '../../hooks/useEscapeHandling.dom.ts';
|
||||
import type { ThemeType } from '../../types/Util.std.ts';
|
||||
import type {
|
||||
EmojiParentKey,
|
||||
EmojiVariantKey,
|
||||
} from '../fun/data/emojis.std.ts';
|
||||
import {
|
||||
EMOJI_PARENT_KEY_CONSTANTS,
|
||||
getEmojiDebugLabel,
|
||||
getEmojiParentKeyByVariantKey,
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from '../fun/data/emojis.std.ts';
|
||||
import { strictAssert } from '../../util/assert.std.ts';
|
||||
import { FunStaticEmoji } from '../fun/FunEmoji.dom.tsx';
|
||||
import { useFunEmojiLocalizer } from '../fun/useFunEmojiLocalizer.dom.tsx';
|
||||
import { createLogger } from '../../logging/log.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { mapValues, orderBy } = lodash;
|
||||
|
||||
const log = createLogger('ReactionViewer');
|
||||
|
||||
export type Reaction = {
|
||||
emoji: string;
|
||||
emoji: Emoji.Variant;
|
||||
timestamp: number;
|
||||
from: Pick<
|
||||
ConversationType,
|
||||
@ -60,7 +45,7 @@ export type Reaction = {
|
||||
export type OwnProps = {
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
reactions: Array<Reaction>;
|
||||
pickedReaction?: string;
|
||||
pickedReaction?: Emoji.Variant;
|
||||
onClose?: () => unknown;
|
||||
theme: ThemeType;
|
||||
};
|
||||
@ -70,49 +55,38 @@ export type Props = OwnProps &
|
||||
Pick<AvatarProps, 'i18n'>;
|
||||
|
||||
const DEFAULT_EMOJI_ORDER = [
|
||||
EMOJI_PARENT_KEY_CONSTANTS.RED_HEART,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.THUMBS_DOWN,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.FACE_WITH_TEARS_OF_JOY,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.FACE_WITH_OPEN_MOUTH,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.CRYING_FACE,
|
||||
EMOJI_PARENT_KEY_CONSTANTS.ENRAGED_FACE,
|
||||
Emoji.HEART,
|
||||
Emoji.THUMBS_UP,
|
||||
Emoji.THUMBS_DOWN,
|
||||
Emoji.JOY,
|
||||
Emoji.OPEN_MOUTH,
|
||||
Emoji.CRY,
|
||||
Emoji.RAGE,
|
||||
];
|
||||
|
||||
type ReactionCategory = {
|
||||
count: number;
|
||||
emoji?: string;
|
||||
emoji?: Emoji.Variant;
|
||||
id: string;
|
||||
index: number;
|
||||
};
|
||||
|
||||
type ReactionWithEmojiData = Reaction &
|
||||
Readonly<{
|
||||
parentKey: EmojiParentKey;
|
||||
variantKey: EmojiVariantKey;
|
||||
parent: Emoji.Parent;
|
||||
variant: Emoji.Variant;
|
||||
}>;
|
||||
|
||||
function ReactionViewerEmoji(props: {
|
||||
emojiVariantValue: string | undefined;
|
||||
emoji: Emoji.Variant | null;
|
||||
}): JSX.Element | null {
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
strictAssert(props.emojiVariantValue != null, 'Expected an emoji');
|
||||
|
||||
if (!isEmojiVariantValue(props.emojiVariantValue)) {
|
||||
log.error(
|
||||
`Must be valid emoji variant value, got ${getEmojiDebugLabel(props.emojiVariantValue)}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const emojiVariantKey = getEmojiVariantKeyByValue(props.emojiVariantValue);
|
||||
const emojiVariant = getEmojiVariantByKey(emojiVariantKey);
|
||||
strictAssert(props.emoji != null, 'Missing emoji');
|
||||
return (
|
||||
<FunStaticEmoji
|
||||
role="img"
|
||||
aria-label={emojiLocalizer.getLocaleShortName(emojiVariantKey)}
|
||||
aria-label={Emoji.getDisplayLabel(props.emoji)}
|
||||
size={18}
|
||||
emoji={emojiVariant}
|
||||
emoji={props.emoji}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -133,13 +107,10 @@ export const ReactionViewer = forwardRef<HTMLDivElement, Props>(
|
||||
const reactionsWithEmojiData = useMemo(
|
||||
() =>
|
||||
reactions
|
||||
.map(reaction => {
|
||||
if (!isEmojiVariantValue(reaction.emoji)) {
|
||||
return null;
|
||||
}
|
||||
const variantKey = getEmojiVariantKeyByValue(reaction.emoji);
|
||||
const parentKey = getEmojiParentKeyByVariantKey(variantKey);
|
||||
return { ...reaction, parentKey, variantKey };
|
||||
.map((reaction): ReactionWithEmojiData | null => {
|
||||
const variant = reaction.emoji;
|
||||
const parent = Emoji.getParent(reaction.emoji);
|
||||
return { ...reaction, parent, variant };
|
||||
})
|
||||
.filter((data): data is ReactionWithEmojiData => {
|
||||
return data != null;
|
||||
@ -149,7 +120,7 @@ export const ReactionViewer = forwardRef<HTMLDivElement, Props>(
|
||||
|
||||
const groupedAndSortedReactions = useMemo(() => {
|
||||
const groups = Object.groupBy(reactionsWithEmojiData, data => {
|
||||
return data.parentKey;
|
||||
return data.parent;
|
||||
});
|
||||
|
||||
return mapValues(
|
||||
@ -177,9 +148,9 @@ export const ReactionViewer = forwardRef<HTMLDivElement, Props>(
|
||||
const firstReaction = localUserReaction || groupedReactions[0];
|
||||
strictAssert(firstReaction, 'Missing firstReaction');
|
||||
return {
|
||||
id: firstReaction.parentKey,
|
||||
index: DEFAULT_EMOJI_ORDER.includes(firstReaction.parentKey)
|
||||
? DEFAULT_EMOJI_ORDER.indexOf(firstReaction.parentKey)
|
||||
id: firstReaction.parent,
|
||||
index: DEFAULT_EMOJI_ORDER.includes(firstReaction.parent)
|
||||
? DEFAULT_EMOJI_ORDER.indexOf(firstReaction.parent)
|
||||
: Infinity,
|
||||
emoji: firstReaction.emoji,
|
||||
count: groupedReactions.length,
|
||||
@ -259,7 +230,7 @@ export const ReactionViewer = forwardRef<HTMLDivElement, Props>(
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<ReactionViewerEmoji emojiVariantValue={emoji} />
|
||||
<ReactionViewerEmoji emoji={emoji ?? null} />
|
||||
<span className="module-reaction-viewer__header__button__count">
|
||||
{count}
|
||||
</span>
|
||||
@ -300,7 +271,7 @@ export const ReactionViewer = forwardRef<HTMLDivElement, Props>(
|
||||
)}
|
||||
</div>
|
||||
<div className="module-reaction-viewer__body__row__emoji">
|
||||
<ReactionViewerEmoji emojiVariantValue={emoji} />
|
||||
<ReactionViewerEmoji emoji={emoji} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -44,6 +44,7 @@ import { PaymentEventKind } from '../../types/Payment.std.ts';
|
||||
import type { RenderAudioAttachmentProps } from '../../state/smart/renderAudioAttachment.preload.tsx';
|
||||
import type { PollVoteWithUserType } from '../../state/selectors/message.preload.ts';
|
||||
import { generateAci } from '../../test-helpers/serviceIdUtils.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { isBoolean, noop } = lodash;
|
||||
|
||||
@ -98,7 +99,7 @@ const messageIdToAudioUrl = {
|
||||
|
||||
function getJoyReaction() {
|
||||
return {
|
||||
emoji: '😂',
|
||||
emoji: Emoji.JOY,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552674',
|
||||
phoneNumber: '+14155552674',
|
||||
@ -608,7 +609,7 @@ export function ReactionsWiderMessage(): JSX.Element {
|
||||
timestamp: Date.now() - 180 * 24 * 60 * 60 * 1000,
|
||||
reactions: [
|
||||
{
|
||||
emoji: '👍',
|
||||
emoji: Emoji.getDefaultVariant(Emoji.THUMBS_UP),
|
||||
from: getDefaultConversation({
|
||||
isMe: true,
|
||||
id: '+14155552672',
|
||||
@ -619,7 +620,7 @@ export function ReactionsWiderMessage(): JSX.Element {
|
||||
timestamp: Date.now() - 10,
|
||||
},
|
||||
{
|
||||
emoji: '👍',
|
||||
emoji: Emoji.getDefaultVariant(Emoji.THUMBS_UP),
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552672',
|
||||
phoneNumber: '+14155552672',
|
||||
@ -629,7 +630,7 @@ export function ReactionsWiderMessage(): JSX.Element {
|
||||
timestamp: Date.now() - 10,
|
||||
},
|
||||
{
|
||||
emoji: '👍',
|
||||
emoji: Emoji.getDefaultVariant(Emoji.THUMBS_UP),
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552673',
|
||||
phoneNumber: '+14155552673',
|
||||
@ -639,7 +640,7 @@ export function ReactionsWiderMessage(): JSX.Element {
|
||||
timestamp: Date.now() - 10,
|
||||
},
|
||||
{
|
||||
emoji: '😂',
|
||||
emoji: Emoji.JOY,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552674',
|
||||
phoneNumber: '+14155552674',
|
||||
@ -649,7 +650,7 @@ export function ReactionsWiderMessage(): JSX.Element {
|
||||
timestamp: Date.now() - 10,
|
||||
},
|
||||
{
|
||||
emoji: '😡',
|
||||
emoji: Emoji.RAGE,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552677',
|
||||
phoneNumber: '+14155552677',
|
||||
@ -659,7 +660,7 @@ export function ReactionsWiderMessage(): JSX.Element {
|
||||
timestamp: Date.now() - 10,
|
||||
},
|
||||
{
|
||||
emoji: '👎',
|
||||
emoji: Emoji.getVariant(Emoji.THUMBS_DOWN, Emoji.SkinTone.None),
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552678',
|
||||
phoneNumber: '+14155552678',
|
||||
@ -669,7 +670,7 @@ export function ReactionsWiderMessage(): JSX.Element {
|
||||
timestamp: Date.now() - 10,
|
||||
},
|
||||
{
|
||||
emoji: '❤️',
|
||||
emoji: Emoji.HEART,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552679',
|
||||
phoneNumber: '+14155552679',
|
||||
@ -693,7 +694,7 @@ export function ReactionsShortMessage(): JSX.Element {
|
||||
reactions: [
|
||||
...joyReactions,
|
||||
{
|
||||
emoji: '👍',
|
||||
emoji: Emoji.getDefaultVariant(Emoji.THUMBS_UP),
|
||||
from: getDefaultConversation({
|
||||
isMe: true,
|
||||
id: '+14155552672',
|
||||
@ -704,7 +705,7 @@ export function ReactionsShortMessage(): JSX.Element {
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
emoji: '👍',
|
||||
emoji: Emoji.getDefaultVariant(Emoji.THUMBS_UP),
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552672',
|
||||
phoneNumber: '+14155552672',
|
||||
@ -714,7 +715,7 @@ export function ReactionsShortMessage(): JSX.Element {
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
emoji: '👍',
|
||||
emoji: Emoji.getDefaultVariant(Emoji.THUMBS_UP),
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552673',
|
||||
phoneNumber: '+14155552673',
|
||||
@ -724,7 +725,7 @@ export function ReactionsShortMessage(): JSX.Element {
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
emoji: '😡',
|
||||
emoji: Emoji.RAGE,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552677',
|
||||
phoneNumber: '+14155552677',
|
||||
@ -734,7 +735,7 @@ export function ReactionsShortMessage(): JSX.Element {
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
emoji: '👎',
|
||||
emoji: Emoji.getVariant(Emoji.THUMBS_DOWN, Emoji.SkinTone.None),
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552678',
|
||||
phoneNumber: '+14155552678',
|
||||
@ -744,7 +745,7 @@ export function ReactionsShortMessage(): JSX.Element {
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
emoji: '❤️',
|
||||
emoji: Emoji.HEART,
|
||||
from: getDefaultConversation({
|
||||
id: '+14155552679',
|
||||
phoneNumber: '+14155552679',
|
||||
@ -783,7 +784,7 @@ LabelInGroup.args = {
|
||||
text: 'Hello it is me, the saxophone.',
|
||||
contactNameColor: '260',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelEmoji: Emoji.POULTRY_LEG,
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
@ -799,7 +800,7 @@ LabelInGroupWithLongName.args = {
|
||||
},
|
||||
contactNameColor: '260',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelEmoji: Emoji.POULTRY_LEG,
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
@ -815,7 +816,7 @@ LabelInGroupWithLongNameAndLongMessage.args = {
|
||||
},
|
||||
contactNameColor: '260',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelEmoji: Emoji.POULTRY_LEG,
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
@ -868,7 +869,7 @@ StickerWithLabelInGroup.args = {
|
||||
status: 'sent',
|
||||
contactNameColor: '260',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelEmoji: Emoji.POULTRY_LEG,
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
@ -893,7 +894,7 @@ StickerWithLongNameAndLabelInGroup.args = {
|
||||
},
|
||||
contactNameColor: '280',
|
||||
contactLabel: {
|
||||
labelEmoji: '🍗',
|
||||
labelEmoji: Emoji.POULTRY_LEG,
|
||||
labelString: 'Chicken Taster',
|
||||
},
|
||||
};
|
||||
@ -3408,7 +3409,7 @@ export const StoryReplyEmoji = (): JSX.Element => {
|
||||
storyReplyContext: {
|
||||
authorTitle: conversation.firstName || conversation.title,
|
||||
conversationColor: ConversationColors[0],
|
||||
emoji: '💄',
|
||||
emoji: Emoji.LIPSTICK,
|
||||
isFromMe: false,
|
||||
rawAttachment: fakeAttachment({
|
||||
url: '/fixtures/snow.jpg',
|
||||
|
||||
@ -32,11 +32,12 @@ import type {
|
||||
import { useScrollerLock } from '../../hooks/useScrollLock.dom.tsx';
|
||||
import { MessageContextMenu } from './MessageContextMenu.dom.tsx';
|
||||
import { ForwardMessagesModalType } from '../ForwardMessagesModal.dom.tsx';
|
||||
import { useGroupedAndOrderedReactions } from '../../util/groupAndOrderReactions.dom.ts';
|
||||
import { useGroupedAndOrderedReactions } from '../../util/groupAndOrderReactions.std.ts';
|
||||
import { isNotNil } from '../../util/isNotNil.std.ts';
|
||||
import type { AxoMenuBuilder } from '../../axo/AxoMenuBuilder.dom.tsx';
|
||||
import { AxoContextMenu } from '../../axo/AxoContextMenu.dom.tsx';
|
||||
import { useDocumentKeyDown } from '../../hooks/useDocumentKeyDown.dom.ts';
|
||||
import type { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { useAxoContextMenuOutsideKeyboardTrigger } = AxoContextMenu;
|
||||
|
||||
@ -53,7 +54,7 @@ export type PropsData = {
|
||||
canReact: boolean;
|
||||
canReply: boolean;
|
||||
canPinMessage: boolean;
|
||||
selectedReaction?: string;
|
||||
selectedReaction?: Emoji.Variant;
|
||||
isTargeted?: boolean;
|
||||
isSignalConversation: boolean;
|
||||
} & Omit<MessagePropsData, 'renderingContext' | 'menu'>;
|
||||
@ -66,7 +67,7 @@ export type PropsActions = {
|
||||
endPoll: (id: string) => void;
|
||||
reactToMessage: (
|
||||
id: string,
|
||||
{ emoji, remove }: { emoji: string; remove: boolean }
|
||||
{ emoji, remove }: { emoji: Emoji.Variant; remove: boolean }
|
||||
) => void;
|
||||
retryMessageSend: (id: string) => void;
|
||||
sendPollVote: (params: {
|
||||
@ -309,13 +310,13 @@ export function TimelineMessage(props: Props): JSX.Element {
|
||||
|
||||
const groupedReactions = useGroupedAndOrderedReactions(
|
||||
props.reactions,
|
||||
'variantKey'
|
||||
'variant'
|
||||
);
|
||||
|
||||
const messageEmojis = useMemo(() => {
|
||||
return groupedReactions
|
||||
.map(groupedReaction => {
|
||||
return groupedReaction?.[0]?.variantKey;
|
||||
return groupedReaction?.[0]?.variant;
|
||||
})
|
||||
.filter(isNotNil);
|
||||
}, [groupedReactions]);
|
||||
|
||||
@ -22,6 +22,7 @@ import type { ContactNameColorType } from '../../../types/Colors.std.ts';
|
||||
import { ContactNameColors } from '../../../types/Colors.std.ts';
|
||||
import { isNotNil } from '../../../util/isNotNil.std.ts';
|
||||
import { strictAssert } from '../../../util/assert.std.ts';
|
||||
import { Emoji } from '../../../axo/emoji.std.ts';
|
||||
|
||||
const { times } = lodash;
|
||||
|
||||
@ -48,7 +49,7 @@ const createProps = (
|
||||
): Props => {
|
||||
const memberships = times(32, i => ({
|
||||
isAdmin: i === 1,
|
||||
labelEmoji: i % 6 === 0 ? '🟢' : undefined,
|
||||
labelEmoji: i % 6 === 0 ? Emoji.GREEN_CIRCLE : undefined,
|
||||
labelString: i % 3 === 0 ? `Task Wrangler ${i}` : undefined,
|
||||
member: getDefaultConversation({
|
||||
isMe: i === 2,
|
||||
@ -220,7 +221,7 @@ export function AsLastAdmin(): JSX.Element {
|
||||
isAdmin
|
||||
memberships={times(32, i => ({
|
||||
isAdmin: i === 2,
|
||||
labelEmoji: i % 6 === 0 ? '🟢' : undefined,
|
||||
labelEmoji: i % 6 === 0 ? Emoji.GREEN_CIRCLE : undefined,
|
||||
labelString: i % 3 === 0 ? `Last Admin ${i}` : undefined,
|
||||
member: getDefaultConversation({
|
||||
isMe: i === 2,
|
||||
|
||||
@ -15,6 +15,7 @@ import type {
|
||||
import { ConversationDetailsMembershipList } from './ConversationDetailsMembershipList.dom.tsx';
|
||||
import type { ContactNameColorType } from '../../../types/Colors.std.ts';
|
||||
import { ContactNameColors } from '../../../types/Colors.std.ts';
|
||||
import { Emoji } from '../../../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -24,7 +25,7 @@ const createMemberships = (
|
||||
return Array.from(new Array(numberOfMemberships)).map(
|
||||
(_, i): GroupV2Membership => ({
|
||||
isAdmin: i % 4 === 0,
|
||||
labelEmoji: i % 6 === 0 ? '🟢' : undefined,
|
||||
labelEmoji: i % 6 === 0 ? Emoji.GREEN_CIRCLE : undefined,
|
||||
labelString: i % 3 === 0 ? `Task Wrangler ${i}` : undefined,
|
||||
member: getDefaultConversation({
|
||||
isMe: i === 2,
|
||||
|
||||
@ -20,11 +20,12 @@ import { GroupMemberLabel } from '../ContactName.dom.tsx';
|
||||
import { AriaClickable } from '../../../axo/AriaClickable.dom.tsx';
|
||||
import type { ContactModalStateType } from '../../../types/globalModals.std.ts';
|
||||
import type { ContactNameColorType } from '../../../types/Colors.std.ts';
|
||||
import type { Emoji } from '../../../axo/emoji.std.ts';
|
||||
|
||||
export type GroupV2Membership = {
|
||||
isAdmin: boolean;
|
||||
member: ConversationType;
|
||||
labelEmoji: string | undefined;
|
||||
labelEmoji: Emoji.Variant | undefined;
|
||||
labelString: string | undefined;
|
||||
};
|
||||
|
||||
|
||||
@ -16,16 +16,36 @@ import { SECOND } from '../../../util/durations/constants.std.ts';
|
||||
import { sleep } from '../../../util/sleep.std.ts';
|
||||
import { SignalService as Proto } from '../../../protobuf/index.std.ts';
|
||||
import { ContactNameColors } from '../../../types/Colors.std.ts';
|
||||
import { Emoji } from '../../../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
function getRandomEmoji() {
|
||||
return sample([
|
||||
Emoji.BLACK_CIRCLE,
|
||||
Emoji.HEART,
|
||||
Emoji.DOTTED_LINE_FACE,
|
||||
Emoji.WHITE_HEART,
|
||||
Emoji.TWO,
|
||||
Emoji.THREE,
|
||||
Emoji.CLINKING_GLASSES,
|
||||
Emoji.CONFETTI_BALL,
|
||||
Emoji.PLUS,
|
||||
Emoji.FACE_WITH_SPIRAL_EYES,
|
||||
Emoji.BIKE,
|
||||
Emoji.DOG,
|
||||
Emoji.CAT,
|
||||
Emoji.HOUSE,
|
||||
]);
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/ConversationDetails/GroupMemberLabelEditor',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
const createProps = (): PropsType => ({
|
||||
canAddLabel: true,
|
||||
existingLabelEmoji: '🐘',
|
||||
existingLabelEmoji: Emoji.ELEPHANT,
|
||||
existingLabelString: 'Good Memory',
|
||||
getPreferredBadge: () => undefined,
|
||||
group: getDefaultConversation({ type: 'group' }),
|
||||
@ -134,22 +154,7 @@ export function AFewMembersWithLabel(): JSX.Element {
|
||||
(contactNameColor, i) => ({
|
||||
member: getDefaultConversation(),
|
||||
isAdmin: i <= 2,
|
||||
labelEmoji: sample([
|
||||
'⚫',
|
||||
'❤️',
|
||||
'🫥',
|
||||
'🤍',
|
||||
'2️⃣',
|
||||
'3️⃣',
|
||||
'🥂',
|
||||
'🎊',
|
||||
'➕',
|
||||
'😵💫',
|
||||
'🚲',
|
||||
'🐶',
|
||||
'🐱',
|
||||
'🏠',
|
||||
]),
|
||||
labelEmoji: getRandomEmoji(),
|
||||
labelString:
|
||||
i % 2 === 0
|
||||
? `Label number long long long long long long long long long ${i}`
|
||||
@ -170,22 +175,7 @@ export function LotsOfMembersWithLabel(): JSX.Element {
|
||||
membersWithLabel={ContactNameColors.map((contactNameColor, i) => ({
|
||||
member: getDefaultConversation(),
|
||||
isAdmin: i <= 6,
|
||||
labelEmoji: sample([
|
||||
'⚫',
|
||||
'❤️',
|
||||
'🫥',
|
||||
'🤍',
|
||||
'2️⃣',
|
||||
'3️⃣',
|
||||
'🥂',
|
||||
'🎊',
|
||||
'➕',
|
||||
'😵💫',
|
||||
'🚲',
|
||||
'🐶',
|
||||
'🐱',
|
||||
'🏠',
|
||||
]),
|
||||
labelEmoji: getRandomEmoji(),
|
||||
labelString:
|
||||
i % 2 === 0
|
||||
? `Label number long long long long long long long long long ${i}`
|
||||
|
||||
@ -6,11 +6,6 @@ import { noop } from 'lodash';
|
||||
|
||||
import { Input } from '../../Input.dom.tsx';
|
||||
import { FunEmojiPicker } from '../../fun/FunEmojiPicker.dom.tsx';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from '../../fun/data/emojis.std.ts';
|
||||
import { FunEmojiPickerButton } from '../../fun/FunButton.dom.tsx';
|
||||
import { tw } from '../../../axo/tw.dom.tsx';
|
||||
import { AxoButton } from '../../../axo/AxoButton.dom.tsx';
|
||||
@ -33,8 +28,6 @@ import { GroupMemberLabel } from '../ContactName.dom.tsx';
|
||||
import { useConfirmDiscard } from '../../../hooks/useConfirmDiscard.dom.tsx';
|
||||
import { NavTab } from '../../../types/Nav.std.ts';
|
||||
import { PanelType } from '../../../types/Panels.std.ts';
|
||||
|
||||
import type { EmojiVariantKey } from '../../fun/data/emojis.std.ts';
|
||||
import type {
|
||||
ConversationType,
|
||||
UpdateGroupMemberLabelType,
|
||||
@ -43,10 +36,11 @@ import type { LocalizerType, ThemeType } from '../../../types/Util.std.ts';
|
||||
import type { PreferredBadgeSelectorType } from '../../../state/selectors/badges.preload.ts';
|
||||
import type { Location } from '../../../types/Nav.std.ts';
|
||||
import { usePrevious } from '../../../hooks/usePrevious.std.ts';
|
||||
import type { Emoji } from '../../../axo/emoji.std.ts';
|
||||
|
||||
export type PropsDataType = {
|
||||
canAddLabel: boolean;
|
||||
existingLabelEmoji: string | undefined;
|
||||
existingLabelEmoji: Emoji.Variant | undefined;
|
||||
existingLabelString: string | undefined;
|
||||
group: ConversationType;
|
||||
i18n: LocalizerType;
|
||||
@ -55,7 +49,7 @@ export type PropsDataType = {
|
||||
membersWithLabel: Array<{
|
||||
contactNameColor: ContactNameColorType;
|
||||
isAdmin: boolean;
|
||||
labelEmoji: string | undefined;
|
||||
labelEmoji: Emoji.Variant | undefined;
|
||||
labelString: string;
|
||||
member: ConversationType;
|
||||
}>;
|
||||
@ -84,14 +78,6 @@ export function getLeafPanelOnly(
|
||||
);
|
||||
}
|
||||
|
||||
function getEmojiVariantKey(value: string): EmojiVariantKey | undefined {
|
||||
if (isEmojiVariantValue(value)) {
|
||||
return getEmojiVariantKeyByValue(value);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function GroupMemberLabelEditor({
|
||||
canAddLabel,
|
||||
group,
|
||||
@ -118,7 +104,6 @@ export function GroupMemberLabelEditor({
|
||||
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
|
||||
|
||||
const emojiKey = labelEmoji ? getEmojiVariantKey(labelEmoji) : null;
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const labelStringForSave = labelString ? labelString.trim() : labelString;
|
||||
@ -185,14 +170,12 @@ export function GroupMemberLabelEditor({
|
||||
onOpenChange={(open: boolean) => setEmojiPickerOpen(open)}
|
||||
placement="bottom"
|
||||
onSelectEmoji={data => {
|
||||
const newEmoji = getEmojiVariantByKey(data.variantKey)?.value;
|
||||
|
||||
setLabelEmoji(newEmoji);
|
||||
setLabelEmoji(data.emoji);
|
||||
}}
|
||||
closeOnSelect
|
||||
theme={theme}
|
||||
>
|
||||
<FunEmojiPickerButton i18n={i18n} selectedEmoji={emojiKey} />
|
||||
<FunEmojiPickerButton i18n={i18n} selectedEmoji={labelEmoji} />
|
||||
</FunEmojiPicker>
|
||||
}
|
||||
maxLengthCount={STRING_GRAPHEME_LIMIT}
|
||||
|
||||
@ -1,15 +1,11 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { useMemo, type JSX } from 'react';
|
||||
import { type JSX } from 'react';
|
||||
import { VisuallyHidden } from 'react-aria';
|
||||
import { Button } from 'react-aria-components';
|
||||
import type { LocalizerType } from '../../types/I18N.std.ts';
|
||||
import {
|
||||
type EmojiVariantKey,
|
||||
getEmojiVariantByKey,
|
||||
} from './data/emojis.std.ts';
|
||||
import { FunStaticEmoji } from './FunEmoji.dom.tsx';
|
||||
import { useFunEmojiLocalizer } from './useFunEmojiLocalizer.dom.tsx';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
/**
|
||||
* Fun Picker Button
|
||||
@ -34,7 +30,7 @@ export function FunPickerButton(props: FunPickerButtonProps): JSX.Element {
|
||||
*/
|
||||
|
||||
export type FunEmojiPickerButtonProps = Readonly<{
|
||||
selectedEmoji?: EmojiVariantKey | null;
|
||||
selectedEmoji?: Emoji.Variant | null;
|
||||
i18n: LocalizerType;
|
||||
}>;
|
||||
|
||||
@ -42,26 +38,15 @@ export function FunEmojiPickerButton(
|
||||
props: FunEmojiPickerButtonProps
|
||||
): JSX.Element {
|
||||
const { i18n } = props;
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
|
||||
const emojiVarant = useMemo(() => {
|
||||
if (props.selectedEmoji == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const variantKey = props.selectedEmoji;
|
||||
const variant = getEmojiVariantByKey(variantKey);
|
||||
return variant;
|
||||
}, [props.selectedEmoji]);
|
||||
|
||||
return (
|
||||
<Button className="FunButton">
|
||||
{emojiVarant ? (
|
||||
{props.selectedEmoji != null ? (
|
||||
<FunStaticEmoji
|
||||
role="img"
|
||||
size={20}
|
||||
aria-label={emojiLocalizer.getLocaleShortName(emojiVarant.key)}
|
||||
emoji={emojiVarant}
|
||||
aria-label={Emoji.getDisplayLabel(props.selectedEmoji)}
|
||||
emoji={props.selectedEmoji}
|
||||
/>
|
||||
) : (
|
||||
<span className="FunButton__Icon FunButton__Icon--EmojiPicker" />
|
||||
|
||||
@ -6,13 +6,7 @@ import { useCallback, useEffect, useRef, type JSX } from 'react';
|
||||
import { type ComponentMeta } from '../../storybook/types.std.ts';
|
||||
import type { FunStaticEmojiProps } from './FunEmoji.dom.tsx';
|
||||
import { FunInlineEmoji, FunStaticEmoji } from './FunEmoji.dom.tsx';
|
||||
import {
|
||||
_getAllEmojiVariantKeys,
|
||||
EMOJI_VARIANT_KEY_CONSTANTS,
|
||||
getEmojiParentByKey,
|
||||
getEmojiParentKeyByVariantKey,
|
||||
getEmojiVariantByKey,
|
||||
} from './data/emojis.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { chunk } = lodash;
|
||||
|
||||
@ -31,9 +25,11 @@ const COLUMNS = 8;
|
||||
|
||||
type AllProps = Pick<FunStaticEmojiProps, 'size'>;
|
||||
|
||||
const ALL_VARIANTS = Array.from(Emoji.iterateAllVariants());
|
||||
|
||||
export function All(props: AllProps): JSX.Element {
|
||||
const scrollerRef = useRef<HTMLDivElement>(null);
|
||||
const data = Array.from(_getAllEmojiVariantKeys());
|
||||
const data = ALL_VARIANTS;
|
||||
const rows = chunk(data, COLUMNS);
|
||||
|
||||
const getScrollElement = useCallback(() => {
|
||||
@ -95,19 +91,15 @@ export function All(props: AllProps): JSX.Element {
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{row.map(emojiVariantKey => {
|
||||
const variant = getEmojiVariantByKey(emojiVariantKey);
|
||||
const parentKey =
|
||||
getEmojiParentKeyByVariantKey(emojiVariantKey);
|
||||
const parent = getEmojiParentByKey(parentKey);
|
||||
{row.map(variant => {
|
||||
return (
|
||||
<div
|
||||
key={emojiVariantKey}
|
||||
key={variant}
|
||||
style={{ display: 'flex', outline: '1px solid' }}
|
||||
>
|
||||
<FunStaticEmoji
|
||||
role="img"
|
||||
aria-label={parent.englishShortNameDefault}
|
||||
aria-label={Emoji.getDisplayLabel(variant)}
|
||||
size={props.size}
|
||||
emoji={variant}
|
||||
/>
|
||||
@ -122,10 +114,6 @@ export function All(props: AllProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const FRIED_SHRIMP = getEmojiVariantByKey(
|
||||
EMOJI_VARIANT_KEY_CONSTANTS.FRIED_SHRIMP
|
||||
);
|
||||
|
||||
export function Inline(): JSX.Element {
|
||||
return (
|
||||
<div style={{ userSelect: 'none' }}>
|
||||
@ -133,7 +121,7 @@ export function Inline(): JSX.Element {
|
||||
<FunInlineEmoji
|
||||
role="img"
|
||||
aria-label="Fried Shrimp"
|
||||
emoji={FRIED_SHRIMP}
|
||||
emoji={Emoji.FRIED_SHRIMP}
|
||||
/>{' '}
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Repellat
|
||||
voluptates, mollitia tempora alias libero repudiandae nesciunt. Deleniti
|
||||
@ -142,7 +130,7 @@ export function Inline(): JSX.Element {
|
||||
<FunInlineEmoji
|
||||
role="img"
|
||||
aria-label="Fried Shrimp"
|
||||
emoji={FRIED_SHRIMP}
|
||||
emoji={Emoji.FRIED_SHRIMP}
|
||||
/>{' '}
|
||||
Consectetur quibusdam accusantium magni ipsum nemo eligendi quisquam
|
||||
dolor, recusandae vero dolore reiciendis doloribus ducimus officiis
|
||||
@ -151,7 +139,7 @@ export function Inline(): JSX.Element {
|
||||
<FunInlineEmoji
|
||||
role="img"
|
||||
aria-label="Fried Shrimp"
|
||||
emoji={FRIED_SHRIMP}
|
||||
emoji={Emoji.FRIED_SHRIMP}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -4,14 +4,9 @@ import classNames from 'classnames';
|
||||
import type { CSSProperties, JSX } from 'react';
|
||||
import { useMemo, useState, useCallback } from 'react';
|
||||
import MANIFEST from '../../../build/jumbomoji.json';
|
||||
import {
|
||||
getEmojiDebugLabel,
|
||||
isSafeEmojifyEmoji,
|
||||
type EmojiVariantData,
|
||||
type EmojiVariantValue,
|
||||
} from './data/emojis.std.ts';
|
||||
import type { FunImageAriaProps } from './types.dom.tsx';
|
||||
import { createLogger } from '../../logging/log.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const log = createLogger('FunEmoji');
|
||||
|
||||
@ -28,14 +23,14 @@ const KNOWN_JUMBOMOJI = new Set<string>(Object.values(MANIFEST).flat());
|
||||
const MIN_JUMBOMOJI_SIZE = 33;
|
||||
|
||||
function getEmojiJumboUrl(
|
||||
emoji: EmojiVariantData,
|
||||
emoji: Emoji.Variant,
|
||||
size: number | undefined
|
||||
): string | null {
|
||||
if (size != null && size < MIN_JUMBOMOJI_SIZE) {
|
||||
return null;
|
||||
}
|
||||
if (KNOWN_JUMBOMOJI.has(emoji.value)) {
|
||||
return `emoji://jumbo?emoji=${encodeURIComponent(emoji.value)}`;
|
||||
if (KNOWN_JUMBOMOJI.has(emoji)) {
|
||||
return `emoji://jumbo?emoji=${encodeURIComponent(emoji)}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -84,7 +79,7 @@ const funStaticEmojiSizeClasses = {
|
||||
export type FunStaticEmojiProps = FunImageAriaProps &
|
||||
Readonly<{
|
||||
size: FunStaticEmojiSize;
|
||||
emoji: EmojiVariantData;
|
||||
emoji: Emoji.Variant;
|
||||
}>;
|
||||
|
||||
export function FunStaticEmoji(props: FunStaticEmojiProps): JSX.Element {
|
||||
@ -103,8 +98,7 @@ export function FunStaticEmoji(props: FunStaticEmojiProps): JSX.Element {
|
||||
height={props.size}
|
||||
role={props.role}
|
||||
aria-label={props['aria-label']}
|
||||
data-emoji-key={props.emoji.key}
|
||||
data-emoji-value={props.emoji.value}
|
||||
data-emoji={props.emoji}
|
||||
className={classNames(
|
||||
FUN_STATIC_EMOJI_CLASS,
|
||||
funStaticEmojiSizeClasses[props.size]
|
||||
@ -122,8 +116,7 @@ export function FunStaticEmoji(props: FunStaticEmojiProps): JSX.Element {
|
||||
<div
|
||||
role={props.role}
|
||||
aria-label={props['aria-label']}
|
||||
data-emoji-key={props.emoji.key}
|
||||
data-emoji-value={props.emoji.value}
|
||||
data-emoji={props.emoji}
|
||||
className={classNames(
|
||||
FUN_STATIC_EMOJI_CLASS,
|
||||
FUN_STATIC_EMOJI_TEXT_CLASS,
|
||||
@ -135,7 +128,7 @@ export function FunStaticEmoji(props: FunStaticEmojiProps): JSX.Element {
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
{props.emoji.value}
|
||||
{props.emoji}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@ -162,16 +155,15 @@ export function createStaticEmojiBlot(
|
||||
node.setAttribute('aria-label', props['aria-label']);
|
||||
}
|
||||
// Needed to lookup emoji value in `matchEmojiBlot`
|
||||
node.dataset.emojiKey = props.emoji.key;
|
||||
node.dataset.emojiValue = props.emoji.value;
|
||||
node.dataset.emoji = props.emoji;
|
||||
|
||||
node.innerText = props.emoji.value;
|
||||
node.innerText = props.emoji;
|
||||
}
|
||||
|
||||
export type FunInlineEmojiProps = FunImageAriaProps &
|
||||
Readonly<{
|
||||
size?: number | null;
|
||||
emoji: EmojiVariantData;
|
||||
emoji: Emoji.Variant;
|
||||
style?: CSSProperties;
|
||||
}>;
|
||||
|
||||
@ -210,8 +202,7 @@ export function FunInlineEmoji(props: FunInlineEmojiProps): JSX.Element {
|
||||
className={FUN_INLINE_EMOJI_CLASS}
|
||||
aria-label={props['aria-label']}
|
||||
// Needed to lookup emoji value in `matchEmojiBlot`
|
||||
data-emoji-key={props.emoji.key}
|
||||
data-emoji-value={props.emoji.value}
|
||||
data-emoji={props.emoji}
|
||||
style={
|
||||
{
|
||||
'--fun-inline-emoji-size':
|
||||
@ -220,9 +211,9 @@ export function FunInlineEmoji(props: FunInlineEmojiProps): JSX.Element {
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<div className={FUN_INLINE_EMOJI_SMALL_CLASS}>{props.emoji.value}</div>
|
||||
<div className={FUN_INLINE_EMOJI_SMALL_CLASS}>{props.emoji}</div>
|
||||
<div className={FUN_INLINE_EMOJI_JUMBO_CLASS}>
|
||||
{!isLoaded && props.emoji.value}
|
||||
{!isLoaded && props.emoji}
|
||||
{img}
|
||||
</div>
|
||||
</div>
|
||||
@ -238,23 +229,23 @@ function isFunEmojiElement(element: HTMLElement): boolean {
|
||||
|
||||
export function getFunEmojiElementValue(
|
||||
element: HTMLElement
|
||||
): EmojiVariantValue | null {
|
||||
): Emoji.Variant | null {
|
||||
if (!isFunEmojiElement(element)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = element.dataset.emojiValue;
|
||||
const value = element.dataset.emoji;
|
||||
if (value == null) {
|
||||
log.error('Missing a data-emoji-value attribute on emoji element');
|
||||
log.error('Missing a data-emoji attribute on emoji element');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isSafeEmojifyEmoji(value)) {
|
||||
if (!Emoji.isEmoji(value)) {
|
||||
log.error(
|
||||
`Expected a valid emoji variant value, got ${getEmojiDebugLabel(value)}`
|
||||
`Expected a valid emoji variant value, got ${Emoji.getDebugLabel(value)}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return value;
|
||||
return Emoji.ignorePreferredSkinTone(value);
|
||||
}
|
||||
|
||||
@ -1,152 +0,0 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import {
|
||||
createContext,
|
||||
memo,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type JSX,
|
||||
} from 'react';
|
||||
import type { LocaleEmojiListType } from '../../types/emoji.std.ts';
|
||||
import { createLogger } from '../../logging/log.std.ts';
|
||||
import * as Errors from '../../types/errors.std.ts';
|
||||
import { drop } from '../../util/drop.std.ts';
|
||||
import {
|
||||
getEmojiDefaultEnglishLocalizerIndex,
|
||||
getEmojiDefaultEnglishSearchIndex,
|
||||
} from './data/emojis.std.ts';
|
||||
import {
|
||||
createFunEmojiLocalizerIndex,
|
||||
type FunEmojiLocalizerIndex,
|
||||
} from './useFunEmojiLocalizer.dom.tsx';
|
||||
import {
|
||||
createFunEmojiSearchIndex,
|
||||
type FunEmojiSearchIndex,
|
||||
} from './useFunEmojiSearch.dom.tsx';
|
||||
import type { LocalizerType } from '../../types/I18N.std.ts';
|
||||
import { strictAssert } from '../../util/assert.std.ts';
|
||||
import { isTestOrMockEnvironment } from '../../environment.std.ts';
|
||||
|
||||
const log = createLogger('FunEmojiLocalizationProvider');
|
||||
|
||||
export type FunEmojiLocalizationContextType = Readonly<{
|
||||
emojiSearchIndex: FunEmojiSearchIndex;
|
||||
emojiLocalizerIndex: FunEmojiLocalizerIndex;
|
||||
}>;
|
||||
|
||||
const FunEmojiLocalizationContext =
|
||||
createContext<FunEmojiLocalizationContextType | null>(null);
|
||||
|
||||
export function useFunEmojiLocalization(): FunEmojiLocalizationContextType {
|
||||
const fun = useContext(FunEmojiLocalizationContext);
|
||||
strictAssert(
|
||||
fun != null,
|
||||
'Must be wrapped with <FunEmojiLocalizationProvider>'
|
||||
);
|
||||
return fun;
|
||||
}
|
||||
|
||||
export type FunEmojiLocalizationProviderProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export const FunEmojiLocalizationProvider = memo(
|
||||
function FunEmojiLocalizationProvider(
|
||||
props: FunEmojiLocalizationProviderProps
|
||||
) {
|
||||
const localeEmojiList = useLocaleEmojiList(props.i18n);
|
||||
const emojiSearchIndex = useFunEmojiSearchIndex(localeEmojiList);
|
||||
const emojiLocalizerIndex = useFunEmojiLocalizerIndex(localeEmojiList);
|
||||
|
||||
const context = useMemo((): FunEmojiLocalizationContextType => {
|
||||
return { emojiSearchIndex, emojiLocalizerIndex };
|
||||
}, [emojiSearchIndex, emojiLocalizerIndex]);
|
||||
|
||||
return (
|
||||
<FunEmojiLocalizationContext.Provider value={context}>
|
||||
{props.children}
|
||||
</FunEmojiLocalizationContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export type FunEmptyEmojiLocalizationProviderProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export function FunDefaultEnglishEmojiLocalizationProvider(
|
||||
props: FunEmptyEmojiLocalizationProviderProps
|
||||
): JSX.Element {
|
||||
const context = useMemo(() => {
|
||||
return {
|
||||
emojiSearchIndex: getEmojiDefaultEnglishSearchIndex(),
|
||||
emojiLocalizerIndex: getEmojiDefaultEnglishLocalizerIndex(),
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<FunEmojiLocalizationContext.Provider value={context}>
|
||||
{props.children}
|
||||
</FunEmojiLocalizationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useLocaleEmojiList(i18n: LocalizerType): LocaleEmojiListType | null {
|
||||
const locale = useMemo(() => i18n.getLocale(), [i18n]);
|
||||
|
||||
const [localeEmojiList, setLocaleEmojiList] =
|
||||
useState<LocaleEmojiListType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
async function run(): Promise<void> {
|
||||
if (isTestOrMockEnvironment()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const list = await window.SignalContext.getLocalizedEmojiList(locale);
|
||||
if (!canceled) {
|
||||
setLocaleEmojiList(list);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`FunProvider: Failed to get localized emoji list for "${locale}"`,
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
drop(run());
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [locale]);
|
||||
|
||||
return localeEmojiList;
|
||||
}
|
||||
|
||||
function useFunEmojiSearchIndex(
|
||||
localeEmojiList: LocaleEmojiListType | null
|
||||
): FunEmojiSearchIndex {
|
||||
const funEmojiSearchIndex = useMemo(() => {
|
||||
const defaultSearchIndex = getEmojiDefaultEnglishSearchIndex();
|
||||
return localeEmojiList != null
|
||||
? createFunEmojiSearchIndex(localeEmojiList, defaultSearchIndex)
|
||||
: defaultSearchIndex;
|
||||
}, [localeEmojiList]);
|
||||
return funEmojiSearchIndex;
|
||||
}
|
||||
|
||||
function useFunEmojiLocalizerIndex(
|
||||
localeEmojiList: LocaleEmojiListType | null
|
||||
): FunEmojiLocalizerIndex {
|
||||
const funEmojiLocalizerIndex = useMemo(() => {
|
||||
const defaultSearchIndex = getEmojiDefaultEnglishLocalizerIndex();
|
||||
return localeEmojiList != null
|
||||
? createFunEmojiLocalizerIndex(localeEmojiList, defaultSearchIndex)
|
||||
: defaultSearchIndex;
|
||||
}, [localeEmojiList]);
|
||||
return funEmojiLocalizerIndex;
|
||||
}
|
||||
@ -12,8 +12,8 @@ import {
|
||||
} from '../../test-helpers/funPickerMocks.dom.tsx';
|
||||
import { FunProvider } from './FunProvider.dom.tsx';
|
||||
import { packs, recentStickers } from '../../test-helpers/stickersMocks.std.ts';
|
||||
import { EmojiSkinTone } from './data/emojis.std.ts';
|
||||
import { Select } from '../Select.dom.tsx';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -23,24 +23,24 @@ type TemplateProps = Omit<
|
||||
>;
|
||||
|
||||
const skinToneOptions = [
|
||||
{ value: EmojiSkinTone.None, text: 'Default' },
|
||||
{ value: EmojiSkinTone.Type1, text: 'Light Skin Tone' },
|
||||
{ value: EmojiSkinTone.Type2, text: 'Medium-Light Skin Tone' },
|
||||
{ value: EmojiSkinTone.Type3, text: 'Medium Skin Tone' },
|
||||
{ value: EmojiSkinTone.Type4, text: 'Medium-Dark Skin Tone' },
|
||||
{ value: EmojiSkinTone.Type5, text: 'Dark Skin Tone' },
|
||||
{ value: Emoji.SkinTone.None, text: 'Default' },
|
||||
{ value: Emoji.SkinTone.Type1, text: 'Light Skin Tone' },
|
||||
{ value: Emoji.SkinTone.Type2, text: 'Medium-Light Skin Tone' },
|
||||
{ value: Emoji.SkinTone.Type3, text: 'Medium Skin Tone' },
|
||||
{ value: Emoji.SkinTone.Type4, text: 'Medium-Dark Skin Tone' },
|
||||
{ value: Emoji.SkinTone.Type5, text: 'Dark Skin Tone' },
|
||||
];
|
||||
|
||||
function Template(props: TemplateProps): JSX.Element {
|
||||
const [open, setOpen] = useState(true);
|
||||
const [skinTone, setSkinTone] = useState(EmojiSkinTone.None);
|
||||
const [skinTone, setSkinTone] = useState(Emoji.SkinTone.None);
|
||||
|
||||
const handleOpenChange = useCallback((openState: boolean) => {
|
||||
setOpen(openState);
|
||||
}, []);
|
||||
|
||||
const handleSkinToneChange = useCallback((value: string) => {
|
||||
setSkinTone(value as EmojiSkinTone);
|
||||
setSkinTone(value as Emoji.SkinTone);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@ -10,7 +10,7 @@ import { FunPanelEmojis } from './panels/FunPanelEmojis.dom.tsx';
|
||||
import { useFunContext } from './FunProvider.dom.tsx';
|
||||
import type { ThemeType } from '../../types/Util.std.ts';
|
||||
import { FunErrorBoundary } from './base/FunErrorBoundary.dom.tsx';
|
||||
import type { EmojiVariantKey } from './data/emojis.std.ts';
|
||||
import type { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
export type FunEmojiPickerProps = Readonly<{
|
||||
open: boolean;
|
||||
@ -21,7 +21,7 @@ export type FunEmojiPickerProps = Readonly<{
|
||||
showCustomizePreferredReactionsButton?: boolean;
|
||||
closeOnSelect: boolean;
|
||||
children: ReactNode;
|
||||
messageEmojis?: ReadonlyArray<EmojiVariantKey>;
|
||||
messageEmojis?: ReadonlyArray<Emoji.Variant>;
|
||||
}>;
|
||||
|
||||
export const FunEmojiPicker = memo(function FunEmojiPicker(
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
MOCK_GIFS_PAGINATED_ONE_PAGE,
|
||||
MOCK_RECENT_EMOJIS,
|
||||
} from '../../test-helpers/funPickerMocks.dom.tsx';
|
||||
import { EmojiSkinTone } from './data/emojis.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -33,7 +33,7 @@ function Template(props: TemplateProps) {
|
||||
recentStickers={recentStickers}
|
||||
recentGifs={[]}
|
||||
// Emojis
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
emojiSkinToneDefault={Emoji.SkinTone.None}
|
||||
onEmojiSkinToneDefaultChange={() => null}
|
||||
onOpenCustomizePreferredReactionsModal={() => null}
|
||||
onSelectEmoji={() => null}
|
||||
|
||||
@ -62,15 +62,15 @@ export const FunPicker = memo(function FunPicker(
|
||||
useEffect(() => {
|
||||
const onKeyDown = createKeybindingsHandler({
|
||||
'$mod+Shift+J': () => {
|
||||
onChangeTab(FunPickerTabKey.Emoji);
|
||||
onChangeTab(FunPickerTabKey.EmojisTab);
|
||||
handleOpenChange(true);
|
||||
},
|
||||
'$mod+Shift+O': () => {
|
||||
onChangeTab(FunPickerTabKey.Stickers);
|
||||
onChangeTab(FunPickerTabKey.StickersTab);
|
||||
handleOpenChange(true);
|
||||
},
|
||||
'$mod+Shift+G': () => {
|
||||
onChangeTab(FunPickerTabKey.Gifs);
|
||||
onChangeTab(FunPickerTabKey.GifsTab);
|
||||
handleOpenChange(true);
|
||||
},
|
||||
});
|
||||
@ -86,17 +86,17 @@ export const FunPicker = memo(function FunPicker(
|
||||
<FunPopover placement={props.placement} theme={props.theme}>
|
||||
<FunTabs value={fun.tab} onChange={fun.onChangeTab}>
|
||||
<FunTabList>
|
||||
<FunPickerTab id={FunPickerTabKey.Emoji}>
|
||||
<FunPickerTab id={FunPickerTabKey.EmojisTab}>
|
||||
{i18n('icu:FunPicker__Tab--Emojis')}
|
||||
</FunPickerTab>
|
||||
<FunPickerTab id={FunPickerTabKey.Stickers}>
|
||||
<FunPickerTab id={FunPickerTabKey.StickersTab}>
|
||||
{i18n('icu:FunPicker__Tab--Stickers')}
|
||||
</FunPickerTab>
|
||||
<FunPickerTab id={FunPickerTabKey.Gifs}>
|
||||
<FunPickerTab id={FunPickerTabKey.GifsTab}>
|
||||
{i18n('icu:FunPicker__Tab--Gifs')}
|
||||
</FunPickerTab>
|
||||
</FunTabList>
|
||||
<FunTabPanel id={FunPickerTabKey.Emoji}>
|
||||
<FunTabPanel id={FunPickerTabKey.EmojisTab}>
|
||||
<FunErrorBoundary>
|
||||
<FunPanelEmojis
|
||||
onSelectEmoji={props.onSelectEmoji}
|
||||
@ -106,7 +106,7 @@ export const FunPicker = memo(function FunPicker(
|
||||
/>
|
||||
</FunErrorBoundary>
|
||||
</FunTabPanel>
|
||||
<FunTabPanel id={FunPickerTabKey.Stickers}>
|
||||
<FunTabPanel id={FunPickerTabKey.StickersTab}>
|
||||
<FunErrorBoundary>
|
||||
<FunPanelStickers
|
||||
showTimeStickers={false}
|
||||
@ -116,7 +116,7 @@ export const FunPicker = memo(function FunPicker(
|
||||
/>
|
||||
</FunErrorBoundary>
|
||||
</FunTabPanel>
|
||||
<FunTabPanel id={FunPickerTabKey.Gifs}>
|
||||
<FunTabPanel id={FunPickerTabKey.GifsTab}>
|
||||
<FunErrorBoundary>
|
||||
<FunPanelGifs
|
||||
onSelectGif={props.onSelectGif}
|
||||
|
||||
@ -20,24 +20,23 @@ import type {
|
||||
fetchGiphySearch,
|
||||
fetchGiphyTrending,
|
||||
} from '../../state/smart/fun/giphy.preload.ts';
|
||||
import type { EmojiSkinTone, EmojiParentKey } from './data/emojis.std.ts';
|
||||
import type { FunGifSelection, GifType } from './panels/FunPanelGifs.dom.tsx';
|
||||
import { FunPickerTabKey } from './constants.dom.tsx';
|
||||
import type { FunEmojiSelection } from './panels/FunPanelEmojis.dom.tsx';
|
||||
import type { FunStickerSelection } from './panels/FunPanelStickers.dom.tsx';
|
||||
import { FunEmojiLocalizationProvider } from './FunEmojiLocalizationProvider.dom.tsx';
|
||||
import type { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
export type FunContextSmartProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
|
||||
// Recents
|
||||
recentEmojis: ReadonlyArray<EmojiParentKey>;
|
||||
recentEmojis: ReadonlyArray<Emoji.Parent>;
|
||||
recentStickers: ReadonlyArray<StickerType>;
|
||||
recentGifs: ReadonlyArray<GifType>;
|
||||
|
||||
// Emojis
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void;
|
||||
emojiSkinToneDefault: Emoji.SkinTone | null;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinTone: Emoji.SkinTone) => void;
|
||||
onOpenCustomizePreferredReactionsModal: () => void;
|
||||
onSelectEmoji: (emojiSelection: FunEmojiSelection) => void;
|
||||
|
||||
@ -99,7 +98,7 @@ export const FunProvider = memo(function FunProvider(
|
||||
props: FunProviderProps
|
||||
): JSX.Element {
|
||||
// Current Tab
|
||||
const [tab, setTab] = useState<FunPickerTabKey>(FunPickerTabKey.Emoji);
|
||||
const [tab, setTab] = useState<FunPickerTabKey>(FunPickerTabKey.EmojisTab);
|
||||
const handleChangeTab = useCallback((key: FunPickerTabKey) => {
|
||||
setTab(key);
|
||||
}, []);
|
||||
@ -130,44 +129,42 @@ export const FunProvider = memo(function FunProvider(
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FunEmojiLocalizationProvider i18n={props.i18n}>
|
||||
<FunProviderInner
|
||||
i18n={props.i18n}
|
||||
// Open state
|
||||
onOpenChange={handleOpenChange}
|
||||
// Current Tab
|
||||
tab={tab}
|
||||
onChangeTab={handleChangeTab}
|
||||
// Search Input
|
||||
storedSearchInput={storedSearchInput}
|
||||
onStoredSearchInputChange={handleStoredSearchInputChange}
|
||||
shouldAutoFocus={shouldAutoFocus}
|
||||
onChangeShouldAutoFocus={handleChangeShouldAutofocus}
|
||||
// Recents
|
||||
recentEmojis={props.recentEmojis}
|
||||
recentStickers={props.recentStickers}
|
||||
recentGifs={props.recentGifs}
|
||||
// Emojis
|
||||
emojiSkinToneDefault={props.emojiSkinToneDefault}
|
||||
onEmojiSkinToneDefaultChange={props.onEmojiSkinToneDefaultChange}
|
||||
onOpenCustomizePreferredReactionsModal={
|
||||
props.onOpenCustomizePreferredReactionsModal
|
||||
}
|
||||
onSelectEmoji={props.onSelectEmoji}
|
||||
// Stickers
|
||||
installedStickerPacks={props.installedStickerPacks}
|
||||
showStickerPickerHint={props.showStickerPickerHint}
|
||||
onClearStickerPickerHint={props.onClearStickerPickerHint}
|
||||
onSelectSticker={props.onSelectSticker}
|
||||
// GIFs
|
||||
fetchGiphyTrending={props.fetchGiphyTrending}
|
||||
fetchGiphySearch={props.fetchGiphySearch}
|
||||
fetchGiphyFile={props.fetchGiphyFile}
|
||||
onRemoveRecentGif={props.onRemoveRecentGif}
|
||||
onSelectGif={props.onSelectGif}
|
||||
>
|
||||
{props.children}
|
||||
</FunProviderInner>
|
||||
</FunEmojiLocalizationProvider>
|
||||
<FunProviderInner
|
||||
i18n={props.i18n}
|
||||
// Open state
|
||||
onOpenChange={handleOpenChange}
|
||||
// Current Tab
|
||||
tab={tab}
|
||||
onChangeTab={handleChangeTab}
|
||||
// Search Input
|
||||
storedSearchInput={storedSearchInput}
|
||||
onStoredSearchInputChange={handleStoredSearchInputChange}
|
||||
shouldAutoFocus={shouldAutoFocus}
|
||||
onChangeShouldAutoFocus={handleChangeShouldAutofocus}
|
||||
// Recents
|
||||
recentEmojis={props.recentEmojis}
|
||||
recentStickers={props.recentStickers}
|
||||
recentGifs={props.recentGifs}
|
||||
// Emojis
|
||||
emojiSkinToneDefault={props.emojiSkinToneDefault}
|
||||
onEmojiSkinToneDefaultChange={props.onEmojiSkinToneDefaultChange}
|
||||
onOpenCustomizePreferredReactionsModal={
|
||||
props.onOpenCustomizePreferredReactionsModal
|
||||
}
|
||||
onSelectEmoji={props.onSelectEmoji}
|
||||
// Stickers
|
||||
installedStickerPacks={props.installedStickerPacks}
|
||||
showStickerPickerHint={props.showStickerPickerHint}
|
||||
onClearStickerPickerHint={props.onClearStickerPickerHint}
|
||||
onSelectSticker={props.onSelectSticker}
|
||||
// GIFs
|
||||
fetchGiphyTrending={props.fetchGiphyTrending}
|
||||
fetchGiphySearch={props.fetchGiphySearch}
|
||||
fetchGiphyFile={props.fetchGiphyFile}
|
||||
onRemoveRecentGif={props.onRemoveRecentGif}
|
||||
onSelectGif={props.onSelectGif}
|
||||
>
|
||||
{props.children}
|
||||
</FunProviderInner>
|
||||
);
|
||||
});
|
||||
|
||||
@ -3,20 +3,16 @@
|
||||
import { useCallback, useMemo, type JSX } from 'react';
|
||||
import type { Selection } from 'react-aria-components';
|
||||
import { ListBox, ListBoxItem } from 'react-aria-components';
|
||||
import type { EmojiParentKey } from './data/emojis.std.ts';
|
||||
import {
|
||||
EmojiSkinTone,
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
} from './data/emojis.std.ts';
|
||||
import { strictAssert } from '../../util/assert.std.ts';
|
||||
import { FunStaticEmoji } from './FunEmoji.dom.tsx';
|
||||
import type { LocalizerType } from '../../types/I18N.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
export type FunSkinTonesListProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
emoji: EmojiParentKey;
|
||||
skinTone: EmojiSkinTone | null;
|
||||
onSelectSkinTone: (skinTone: EmojiSkinTone) => void;
|
||||
emoji: Emoji.Parent;
|
||||
skinTone: Emoji.SkinTone | null;
|
||||
onSelectSkinTone: (skinTone: Emoji.SkinTone) => void;
|
||||
}>;
|
||||
|
||||
export function FunSkinTonesList(props: FunSkinTonesListProps): JSX.Element {
|
||||
@ -27,7 +23,7 @@ export function FunSkinTonesList(props: FunSkinTonesListProps): JSX.Element {
|
||||
strictAssert(keys !== 'all', 'Expected single selection');
|
||||
strictAssert(keys.size === 1, 'Expected single selection');
|
||||
const [first] = keys.values();
|
||||
onSelectSkinTone(first as EmojiSkinTone);
|
||||
onSelectSkinTone(first as Emoji.SkinTone);
|
||||
},
|
||||
[onSelectSkinTone]
|
||||
);
|
||||
@ -45,31 +41,31 @@ export function FunSkinTonesList(props: FunSkinTonesListProps): JSX.Element {
|
||||
<FunSkinTonesListItem
|
||||
emoji={props.emoji}
|
||||
aria-label={i18n('icu:FunSkinTones__ListItem--None')}
|
||||
skinTone={EmojiSkinTone.None}
|
||||
skinTone={Emoji.SkinTone.None}
|
||||
/>
|
||||
<FunSkinTonesListItem
|
||||
emoji={props.emoji}
|
||||
skinTone={EmojiSkinTone.Type1}
|
||||
skinTone={Emoji.SkinTone.Type1}
|
||||
aria-label={i18n('icu:FunSkinTones__ListItem--Light')}
|
||||
/>
|
||||
<FunSkinTonesListItem
|
||||
emoji={props.emoji}
|
||||
skinTone={EmojiSkinTone.Type2}
|
||||
skinTone={Emoji.SkinTone.Type2}
|
||||
aria-label={i18n('icu:FunSkinTones__ListItem--MediumLight')}
|
||||
/>
|
||||
<FunSkinTonesListItem
|
||||
emoji={props.emoji}
|
||||
skinTone={EmojiSkinTone.Type3}
|
||||
skinTone={Emoji.SkinTone.Type3}
|
||||
aria-label={i18n('icu:FunSkinTones__ListItem--Medium')}
|
||||
/>
|
||||
<FunSkinTonesListItem
|
||||
emoji={props.emoji}
|
||||
skinTone={EmojiSkinTone.Type4}
|
||||
skinTone={Emoji.SkinTone.Type4}
|
||||
aria-label={i18n('icu:FunSkinTones__ListItem--MediumDark')}
|
||||
/>
|
||||
<FunSkinTonesListItem
|
||||
emoji={props.emoji}
|
||||
skinTone={EmojiSkinTone.Type5}
|
||||
skinTone={Emoji.SkinTone.Type5}
|
||||
aria-label={i18n('icu:FunSkinTones__ListItem--Dark')}
|
||||
/>
|
||||
</ListBox>
|
||||
@ -77,14 +73,14 @@ export function FunSkinTonesList(props: FunSkinTonesListProps): JSX.Element {
|
||||
}
|
||||
|
||||
type FunSkinTonesListItemProps = Readonly<{
|
||||
emoji: EmojiParentKey;
|
||||
emoji: Emoji.Parent;
|
||||
'aria-label': string;
|
||||
skinTone: EmojiSkinTone;
|
||||
skinTone: Emoji.SkinTone;
|
||||
}>;
|
||||
|
||||
function FunSkinTonesListItem(props: FunSkinTonesListItemProps) {
|
||||
const variant = useMemo(() => {
|
||||
return getEmojiVariantByParentKeyAndSkinTone(props.emoji, props.skinTone);
|
||||
return Emoji.getVariant(props.emoji, props.skinTone);
|
||||
}, [props.emoji, props.skinTone]);
|
||||
|
||||
return (
|
||||
|
||||
@ -9,7 +9,7 @@ import { FunStickerPicker } from './FunStickerPicker.dom.tsx';
|
||||
import { MOCK_RECENT_EMOJIS } from '../../test-helpers/funPickerMocks.dom.tsx';
|
||||
import { FunProvider } from './FunProvider.dom.tsx';
|
||||
import { packs, recentStickers } from '../../test-helpers/stickersMocks.std.ts';
|
||||
import { EmojiSkinTone } from './data/emojis.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -33,7 +33,7 @@ function Template(props: TemplateProps): JSX.Element {
|
||||
recentStickers={recentStickers}
|
||||
recentGifs={[]}
|
||||
// Emojis
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
emojiSkinToneDefault={Emoji.SkinTone.None}
|
||||
onEmojiSkinToneDefaultChange={() => null}
|
||||
onOpenCustomizePreferredReactionsModal={() => null}
|
||||
onSelectEmoji={() => null}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { EmojiPickerCategory } from './data/emojis.std.ts';
|
||||
import type { StickerPackType } from '../../state/ducks/stickers.preload.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
export enum FunPickerTabKey {
|
||||
Emoji = 'Emoji',
|
||||
Stickers = 'Stickers',
|
||||
Gifs = 'Gifs',
|
||||
EmojisTab = 'EmojisTab',
|
||||
StickersTab = 'Stickers',
|
||||
GifsTab = 'Gifs',
|
||||
}
|
||||
|
||||
export enum FunGifsCategory {
|
||||
@ -54,7 +54,7 @@ export function toFunStickersPackSection(
|
||||
|
||||
export type FunEmojisSection =
|
||||
| FunSectionCommon
|
||||
| EmojiPickerCategory
|
||||
| Emoji.Category
|
||||
| FunEmojisBase;
|
||||
export type FunStickersSection =
|
||||
| FunSectionCommon
|
||||
@ -63,16 +63,16 @@ export type FunStickersSection =
|
||||
export type FunGifsSection = FunSectionCommon | FunGifsCategory;
|
||||
|
||||
export const FunEmojisSectionOrder: ReadonlyArray<
|
||||
FunSectionCommon.Recents | FunEmojisBase.ThisMessage | EmojiPickerCategory
|
||||
FunSectionCommon.Recents | FunEmojisBase.ThisMessage | Emoji.Category
|
||||
> = [
|
||||
FunEmojisBase.ThisMessage,
|
||||
FunSectionCommon.Recents,
|
||||
EmojiPickerCategory.SmileysAndPeople,
|
||||
EmojiPickerCategory.AnimalsAndNature,
|
||||
EmojiPickerCategory.FoodAndDrink,
|
||||
EmojiPickerCategory.Activities,
|
||||
EmojiPickerCategory.TravelAndPlaces,
|
||||
EmojiPickerCategory.Objects,
|
||||
EmojiPickerCategory.Symbols,
|
||||
EmojiPickerCategory.Flags,
|
||||
Emoji.Category.SMILIES_AND_PEOPLE,
|
||||
Emoji.Category.ANIMALS_AND_NATURE,
|
||||
Emoji.Category.FOOD_AND_DRINK,
|
||||
Emoji.Category.ACTIVITIES,
|
||||
Emoji.Category.TRAVEL_AND_PLACES,
|
||||
Emoji.Category.OBJECTS,
|
||||
Emoji.Category.SYMBOLS,
|
||||
Emoji.Category.FLAGS,
|
||||
];
|
||||
|
||||
@ -1,695 +0,0 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import emojiRegex from 'emoji-regex';
|
||||
import { strictAssert } from '../../../util/assert.std.ts';
|
||||
import type {
|
||||
FunEmojiSearchIndex,
|
||||
FunEmojiSearchIndexEntry,
|
||||
} from '../useFunEmojiSearch.dom.tsx';
|
||||
import type { FunEmojiLocalizerIndex } from '../useFunEmojiLocalizer.dom.tsx';
|
||||
import { removeDiacritics } from '../../../util/removeDiacritics.std.ts';
|
||||
import { createLogger } from '../../../logging/log.std.ts';
|
||||
import { EmojiSkinTone } from '../../../types/emoji.std.ts';
|
||||
import RAW_EMOJI_DATA from 'emoji-datasource';
|
||||
|
||||
export { EmojiSkinTone } from '../../../types/emoji.std.ts';
|
||||
|
||||
const log = createLogger('fun/data/emojis');
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
|
||||
export enum EmojiUnicodeCategory {
|
||||
SmileysAndEmotion = 'EmojiUnicodeCategory.SmileysAndEmotion',
|
||||
PeopleAndBody = 'EmojiUnicodeCategory.PeopleAndBody',
|
||||
Component = 'EmojiUnicodeCategory.Component',
|
||||
AnimalsAndNature = 'EmojiUnicodeCategory.AnimalsAndNature',
|
||||
FoodAndDrink = 'EmojiUnicodeCategory.FoodAndDrink',
|
||||
TravelAndPlaces = 'EmojiUnicodeCategory.TravelAndPlaces',
|
||||
Activities = 'EmojiUnicodeCategory.Activities',
|
||||
Objects = 'EmojiUnicodeCategory.Objects',
|
||||
Symbols = 'EmojiUnicodeCategory.Symbols',
|
||||
Flags = 'EmojiUnicodeCategory.Flags',
|
||||
}
|
||||
|
||||
export enum EmojiPickerCategory {
|
||||
SmileysAndPeople = 'EmojiPickerCategory.SmileysAndPeople',
|
||||
AnimalsAndNature = 'EmojiPickerCategory.AnimalsAndNature',
|
||||
FoodAndDrink = 'EmojiPickerCategory.FoodAndDrink',
|
||||
TravelAndPlaces = 'EmojiPickerCategory.TravelAndPlaces',
|
||||
Activities = 'EmojiPickerCategory.Activities',
|
||||
Objects = 'EmojiPickerCategory.Objects',
|
||||
Symbols = 'EmojiPickerCategory.Symbols',
|
||||
Flags = 'EmojiPickerCategory.Flags',
|
||||
}
|
||||
|
||||
export function isValidEmojiSkinTone(value: unknown): value is EmojiSkinTone {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
EMOJI_SKIN_TONE_ORDER.includes(value as EmojiSkinTone)
|
||||
);
|
||||
}
|
||||
|
||||
export const EMOJI_SKIN_TONE_ORDER: ReadonlyArray<EmojiSkinTone> = [
|
||||
EmojiSkinTone.None,
|
||||
EmojiSkinTone.Type1,
|
||||
EmojiSkinTone.Type2,
|
||||
EmojiSkinTone.Type3,
|
||||
EmojiSkinTone.Type4,
|
||||
EmojiSkinTone.Type5,
|
||||
];
|
||||
|
||||
/** @deprecated We should use `EmojiSkinTone` everywhere */
|
||||
const KEY_TO_EMOJI_SKIN_TONE = new Map<string, EmojiSkinTone>([
|
||||
['1F3FB', EmojiSkinTone.Type1],
|
||||
['1F3FC', EmojiSkinTone.Type2],
|
||||
['1F3FD', EmojiSkinTone.Type3],
|
||||
['1F3FE', EmojiSkinTone.Type4],
|
||||
['1F3FF', EmojiSkinTone.Type5],
|
||||
]);
|
||||
|
||||
export type EmojiParentKey = string & { EmojiParentKey: never };
|
||||
export type EmojiVariantKey = string & { EmojiVariantKey: never };
|
||||
|
||||
export type EmojiParentValue = string & { EmojiParentValue: never };
|
||||
export type EmojiVariantValue = string & { EmojiVariantValue: never };
|
||||
|
||||
/** @deprecated Prefer EmojiKey for refs, load short names from translations */
|
||||
export type EmojiEnglishShortName = string & { EmojiEnglishShortName: never };
|
||||
|
||||
export type EmojiVariantData = Readonly<{
|
||||
key: EmojiVariantKey;
|
||||
value: EmojiVariantValue;
|
||||
valueNonqualified: EmojiVariantValue | null;
|
||||
sheetX: number;
|
||||
sheetY: number;
|
||||
}>;
|
||||
|
||||
type EmojiDefaultSkinToneVariants = Record<EmojiSkinTone, EmojiVariantKey>;
|
||||
|
||||
export type EmojiParentData = Readonly<{
|
||||
key: EmojiParentKey;
|
||||
value: EmojiParentValue;
|
||||
valueNonqualified: EmojiParentValue | null;
|
||||
unicodeCategory: EmojiUnicodeCategory;
|
||||
pickerCategory: EmojiPickerCategory | null;
|
||||
defaultVariant: EmojiVariantKey;
|
||||
defaultSkinToneVariants: EmojiDefaultSkinToneVariants | null;
|
||||
/** @deprecated Prefer EmojiKey for refs, load short names from translations */
|
||||
englishShortNameDefault: EmojiEnglishShortName;
|
||||
/** @deprecated Prefer EmojiKey for refs, load short names from translations */
|
||||
englishShortNames: ReadonlyArray<EmojiEnglishShortName>;
|
||||
emoticonDefault: string | null;
|
||||
emoticons: ReadonlyArray<string>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Schemas
|
||||
*/
|
||||
|
||||
const RAW_UNICODE_CATEGORY_MAP: Record<string, EmojiUnicodeCategory> = {
|
||||
'Smileys & Emotion': EmojiUnicodeCategory.SmileysAndEmotion,
|
||||
'People & Body': EmojiUnicodeCategory.PeopleAndBody,
|
||||
Component: EmojiUnicodeCategory.Component,
|
||||
'Animals & Nature': EmojiUnicodeCategory.AnimalsAndNature,
|
||||
'Food & Drink': EmojiUnicodeCategory.FoodAndDrink,
|
||||
'Travel & Places': EmojiUnicodeCategory.TravelAndPlaces,
|
||||
Activities: EmojiUnicodeCategory.Activities,
|
||||
Objects: EmojiUnicodeCategory.Objects,
|
||||
Symbols: EmojiUnicodeCategory.Symbols,
|
||||
Flags: EmojiUnicodeCategory.Flags,
|
||||
};
|
||||
|
||||
const RAW_PICKER_CATEGORY_MAP: Record<string, EmojiPickerCategory | null> = {
|
||||
'Smileys & Emotion': EmojiPickerCategory.SmileysAndPeople, // merged
|
||||
'People & Body': EmojiPickerCategory.SmileysAndPeople, // merged
|
||||
Component: null, // dropped
|
||||
'Animals & Nature': EmojiPickerCategory.AnimalsAndNature,
|
||||
'Food & Drink': EmojiPickerCategory.FoodAndDrink,
|
||||
'Travel & Places': EmojiPickerCategory.TravelAndPlaces,
|
||||
Activities: EmojiPickerCategory.Activities,
|
||||
Objects: EmojiPickerCategory.Objects,
|
||||
Symbols: EmojiPickerCategory.Symbols,
|
||||
Flags: EmojiPickerCategory.Flags,
|
||||
};
|
||||
|
||||
/**
|
||||
* Data Normalization
|
||||
*/
|
||||
|
||||
function toEmojiUnicodeCategory(category: string): EmojiUnicodeCategory {
|
||||
const result = RAW_UNICODE_CATEGORY_MAP[category];
|
||||
strictAssert(result != null, `Unknown category: ${category}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function toEmojiPickerCategory(category: string): EmojiPickerCategory | null {
|
||||
const result = RAW_PICKER_CATEGORY_MAP[category];
|
||||
strictAssert(
|
||||
typeof result !== 'undefined',
|
||||
`Unknown picker category: ${category}`
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
function toEmojiParentKey(unified: string): EmojiParentKey {
|
||||
return unified as EmojiParentKey;
|
||||
}
|
||||
|
||||
function toEmojiVariantKey(unified: string): EmojiVariantKey {
|
||||
return unified as EmojiVariantKey;
|
||||
}
|
||||
|
||||
function encodeUnified(unified: string): string {
|
||||
return unified
|
||||
.split('-')
|
||||
.map(char => String.fromCodePoint(Number.parseInt(char, 16)))
|
||||
.join('');
|
||||
}
|
||||
|
||||
function toEmojiParentValue(unified: string): EmojiParentValue {
|
||||
return encodeUnified(unified) as EmojiParentValue;
|
||||
}
|
||||
|
||||
function toEmojiVariantValue(unified: string): EmojiVariantValue {
|
||||
return encodeUnified(unified) as EmojiVariantValue;
|
||||
}
|
||||
|
||||
const WOMAN = '\u{1F469}';
|
||||
const MAN = '\u{1F468}';
|
||||
const GIRL = '\u{1F467}';
|
||||
const BOY = '\u{1F466}';
|
||||
const ZWJ = '\u{200D}';
|
||||
|
||||
/**
|
||||
* Deprecated unicode emoji should continue to be rendered when used,
|
||||
* but should be hidden from emoji pickers.
|
||||
*/
|
||||
const UNICODE_DEPRECATED_EMOJI = new Set<EmojiParentValue>([
|
||||
/**
|
||||
* 2022 - Family Emoji Redesign: Gender Inclusive Variants
|
||||
* https://www.unicode.org/L2/L2023/23029-family-emoji.pdf
|
||||
* https://www.unicode.org/L2/L2022/22276-family-emoji-guidelines.pdf
|
||||
*/
|
||||
|
||||
// 1 ADULT, 1 CHILD
|
||||
`${WOMAN}${ZWJ}${GIRL}`,
|
||||
`${WOMAN}${ZWJ}${BOY}`,
|
||||
`${MAN}${ZWJ}${GIRL}`,
|
||||
`${MAN}${ZWJ}${BOY}`,
|
||||
// 1 ADULT, 2 CHILDREN
|
||||
`${WOMAN}${ZWJ}${GIRL}${ZWJ}${GIRL}`,
|
||||
`${WOMAN}${ZWJ}${GIRL}${ZWJ}${BOY}`,
|
||||
`${WOMAN}${ZWJ}${BOY}${ZWJ}${BOY}`,
|
||||
`${MAN}${ZWJ}${GIRL}${ZWJ}${GIRL}`,
|
||||
`${MAN}${ZWJ}${GIRL}${ZWJ}${BOY}`,
|
||||
`${MAN}${ZWJ}${BOY}${ZWJ}${BOY}`,
|
||||
// 2 ADULTS, 1 CHILD
|
||||
`${WOMAN}${ZWJ}${WOMAN}${ZWJ}${GIRL}`,
|
||||
`${WOMAN}${ZWJ}${WOMAN}${ZWJ}${BOY}`,
|
||||
`${MAN}${ZWJ}${WOMAN}${ZWJ}${GIRL}`,
|
||||
`${MAN}${ZWJ}${WOMAN}${ZWJ}${BOY}`,
|
||||
`${MAN}${ZWJ}${MAN}${ZWJ}${GIRL}`,
|
||||
`${MAN}${ZWJ}${MAN}${ZWJ}${BOY}`,
|
||||
// 2 ADULTS, 2 CHILDREN
|
||||
`${WOMAN}${ZWJ}${WOMAN}${ZWJ}${GIRL}${ZWJ}${GIRL}`,
|
||||
`${WOMAN}${ZWJ}${WOMAN}${ZWJ}${GIRL}${ZWJ}${BOY}`,
|
||||
`${WOMAN}${ZWJ}${WOMAN}${ZWJ}${BOY}${ZWJ}${BOY}`,
|
||||
|
||||
`${MAN}${ZWJ}${WOMAN}${ZWJ}${GIRL}${ZWJ}${GIRL}`,
|
||||
`${MAN}${ZWJ}${WOMAN}${ZWJ}${GIRL}${ZWJ}${BOY}`,
|
||||
`${MAN}${ZWJ}${WOMAN}${ZWJ}${BOY}${ZWJ}${BOY}`,
|
||||
|
||||
`${MAN}${ZWJ}${MAN}${ZWJ}${GIRL}${ZWJ}${GIRL}`,
|
||||
`${MAN}${ZWJ}${MAN}${ZWJ}${GIRL}${ZWJ}${BOY}`,
|
||||
`${MAN}${ZWJ}${MAN}${ZWJ}${BOY}${ZWJ}${BOY}`,
|
||||
] as Array<EmojiParentValue>);
|
||||
|
||||
/** @internal */
|
||||
type EmojiIndex = Readonly<{
|
||||
// raw data
|
||||
parentByKey: Map<EmojiParentKey, EmojiParentData>;
|
||||
parentKeysByName: Map<EmojiEnglishShortName, EmojiParentKey>;
|
||||
parentKeysByValue: Map<EmojiParentValue, EmojiParentKey>;
|
||||
parentKeysByValueNonQualified: Map<EmojiParentValue, EmojiParentKey>;
|
||||
parentKeysByVariantKeys: Map<EmojiVariantKey, EmojiParentKey>;
|
||||
|
||||
variantByKey: Map<EmojiVariantKey, EmojiVariantData>;
|
||||
variantKeysByValue: Map<EmojiVariantValue, EmojiVariantKey>;
|
||||
variantKeysByValueNonQualified: Map<EmojiVariantValue, EmojiVariantKey>;
|
||||
variantKeyToSkinTone: Map<EmojiVariantKey, EmojiSkinTone>;
|
||||
|
||||
unicodeCategories: Record<EmojiUnicodeCategory, Array<EmojiParentKey>>;
|
||||
pickerCategories: Record<EmojiPickerCategory, Array<EmojiParentKey>>;
|
||||
|
||||
defaultEnglishSearchIndex: Array<FunEmojiSearchIndexEntry>;
|
||||
defaultEnglishLocalizerIndex: {
|
||||
parentKeyToLocaleShortName: Map<EmojiParentKey, string>;
|
||||
localeShortNameToParentKey: Map<string, EmojiParentKey>;
|
||||
};
|
||||
}>;
|
||||
|
||||
/** @internal */
|
||||
const EMOJI_INDEX: EmojiIndex = {
|
||||
parentByKey: new Map(),
|
||||
parentKeysByValue: new Map(),
|
||||
parentKeysByValueNonQualified: new Map(),
|
||||
parentKeysByName: new Map(),
|
||||
parentKeysByVariantKeys: new Map(),
|
||||
variantByKey: new Map(),
|
||||
variantKeysByValue: new Map(),
|
||||
variantKeysByValueNonQualified: new Map(),
|
||||
variantKeyToSkinTone: new Map(),
|
||||
unicodeCategories: {
|
||||
[EmojiUnicodeCategory.SmileysAndEmotion]: [],
|
||||
[EmojiUnicodeCategory.PeopleAndBody]: [],
|
||||
[EmojiUnicodeCategory.Component]: [],
|
||||
[EmojiUnicodeCategory.AnimalsAndNature]: [],
|
||||
[EmojiUnicodeCategory.FoodAndDrink]: [],
|
||||
[EmojiUnicodeCategory.TravelAndPlaces]: [],
|
||||
[EmojiUnicodeCategory.Activities]: [],
|
||||
[EmojiUnicodeCategory.Objects]: [],
|
||||
[EmojiUnicodeCategory.Symbols]: [],
|
||||
[EmojiUnicodeCategory.Flags]: [],
|
||||
},
|
||||
pickerCategories: {
|
||||
[EmojiPickerCategory.SmileysAndPeople]: [],
|
||||
[EmojiPickerCategory.AnimalsAndNature]: [],
|
||||
[EmojiPickerCategory.FoodAndDrink]: [],
|
||||
[EmojiPickerCategory.TravelAndPlaces]: [],
|
||||
[EmojiPickerCategory.Activities]: [],
|
||||
[EmojiPickerCategory.Objects]: [],
|
||||
[EmojiPickerCategory.Symbols]: [],
|
||||
[EmojiPickerCategory.Flags]: [],
|
||||
},
|
||||
defaultEnglishSearchIndex: [],
|
||||
defaultEnglishLocalizerIndex: {
|
||||
parentKeyToLocaleShortName: new Map(),
|
||||
localeShortNameToParentKey: new Map(),
|
||||
},
|
||||
};
|
||||
|
||||
function addParent(parent: EmojiParentData, rank: number) {
|
||||
const isDeprecated = UNICODE_DEPRECATED_EMOJI.has(parent.value);
|
||||
|
||||
EMOJI_INDEX.parentByKey.set(parent.key, parent);
|
||||
EMOJI_INDEX.parentKeysByValue.set(parent.value, parent.key);
|
||||
if (parent.valueNonqualified != null) {
|
||||
EMOJI_INDEX.parentKeysByValue.set(parent.valueNonqualified, parent.key);
|
||||
EMOJI_INDEX.parentKeysByValueNonQualified.set(
|
||||
parent.valueNonqualified,
|
||||
parent.key
|
||||
);
|
||||
}
|
||||
EMOJI_INDEX.parentKeysByName.set(parent.englishShortNameDefault, parent.key);
|
||||
EMOJI_INDEX.unicodeCategories[parent.unicodeCategory].push(parent.key);
|
||||
if (parent.pickerCategory != null && !isDeprecated) {
|
||||
EMOJI_INDEX.pickerCategories[parent.pickerCategory].push(parent.key);
|
||||
}
|
||||
|
||||
for (const englishShortName of parent.englishShortNames) {
|
||||
EMOJI_INDEX.parentKeysByName.set(englishShortName, parent.key);
|
||||
}
|
||||
|
||||
if (!isDeprecated) {
|
||||
EMOJI_INDEX.defaultEnglishSearchIndex.push({
|
||||
key: parent.key,
|
||||
rank,
|
||||
shortName: parent.englishShortNameDefault,
|
||||
shortNames: parent.englishShortNames,
|
||||
emoticon: parent.emoticonDefault,
|
||||
emoticons: parent.emoticons,
|
||||
});
|
||||
}
|
||||
|
||||
EMOJI_INDEX.defaultEnglishLocalizerIndex.parentKeyToLocaleShortName.set(
|
||||
parent.key,
|
||||
parent.englishShortNameDefault
|
||||
);
|
||||
EMOJI_INDEX.defaultEnglishLocalizerIndex.localeShortNameToParentKey.set(
|
||||
parent.englishShortNameDefault,
|
||||
parent.key
|
||||
);
|
||||
}
|
||||
|
||||
function addVariant(parentKey: EmojiParentKey, variant: EmojiVariantData) {
|
||||
EMOJI_INDEX.parentKeysByVariantKeys.set(variant.key, parentKey);
|
||||
EMOJI_INDEX.variantByKey.set(variant.key, variant);
|
||||
EMOJI_INDEX.variantKeysByValue.set(variant.value, variant.key);
|
||||
if (variant.valueNonqualified) {
|
||||
EMOJI_INDEX.variantKeysByValue.set(variant.valueNonqualified, variant.key);
|
||||
EMOJI_INDEX.variantKeysByValueNonQualified.set(
|
||||
variant.valueNonqualified,
|
||||
variant.key
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SORTED_EMOJI_DATA = RAW_EMOJI_DATA.toSorted((a, b) => {
|
||||
return a.sort_order - b.sort_order;
|
||||
});
|
||||
|
||||
for (const rawEmoji of SORTED_EMOJI_DATA) {
|
||||
if (!rawEmoji.has_img_apple) {
|
||||
continue;
|
||||
}
|
||||
const parentKey = toEmojiParentKey(rawEmoji.unified);
|
||||
|
||||
const defaultVariant: EmojiVariantData = {
|
||||
key: toEmojiVariantKey(rawEmoji.unified),
|
||||
value: toEmojiVariantValue(rawEmoji.unified),
|
||||
valueNonqualified:
|
||||
rawEmoji.non_qualified != null
|
||||
? toEmojiVariantValue(rawEmoji.non_qualified)
|
||||
: null,
|
||||
sheetX: rawEmoji.sheet_x,
|
||||
sheetY: rawEmoji.sheet_y,
|
||||
};
|
||||
|
||||
addVariant(parentKey, defaultVariant);
|
||||
|
||||
let defaultSkinToneVariants: EmojiDefaultSkinToneVariants | null = null;
|
||||
if (rawEmoji.skin_variations != null) {
|
||||
const map = new Map<string, EmojiVariantKey>();
|
||||
|
||||
for (const [key, value] of Object.entries(rawEmoji.skin_variations)) {
|
||||
if (!value.has_img_apple) {
|
||||
continue;
|
||||
}
|
||||
const variantKey = toEmojiVariantKey(value.unified);
|
||||
map.set(key, variantKey);
|
||||
|
||||
const skinToneVariant: EmojiVariantData = {
|
||||
key: variantKey,
|
||||
value: toEmojiVariantValue(value.unified),
|
||||
valueNonqualified:
|
||||
rawEmoji.non_qualified != null
|
||||
? toEmojiVariantValue(rawEmoji.non_qualified)
|
||||
: null,
|
||||
sheetX: value.sheet_x,
|
||||
sheetY: value.sheet_y,
|
||||
};
|
||||
|
||||
addVariant(parentKey, skinToneVariant);
|
||||
}
|
||||
|
||||
const result: Partial<EmojiDefaultSkinToneVariants> = {};
|
||||
for (const [key, skinTone] of KEY_TO_EMOJI_SKIN_TONE) {
|
||||
const one = map.get(key) ?? null;
|
||||
const two = map.get(`${key}-${key}`) ?? null;
|
||||
const variantKey = one ?? two;
|
||||
if (variantKey == null) {
|
||||
const keys = Object.keys(rawEmoji.skin_variations);
|
||||
throw new Error(
|
||||
`Missing variant key ${parentKey} -> ${key} (${keys.join(', ')})`
|
||||
);
|
||||
}
|
||||
result[skinTone] = variantKey;
|
||||
EMOJI_INDEX.variantKeyToSkinTone.set(variantKey, skinTone);
|
||||
}
|
||||
|
||||
defaultSkinToneVariants = result as EmojiDefaultSkinToneVariants;
|
||||
}
|
||||
|
||||
const parent: EmojiParentData = {
|
||||
key: toEmojiParentKey(rawEmoji.unified),
|
||||
value: toEmojiParentValue(rawEmoji.unified),
|
||||
valueNonqualified:
|
||||
rawEmoji.non_qualified != null
|
||||
? toEmojiParentValue(rawEmoji.non_qualified)
|
||||
: null,
|
||||
unicodeCategory: toEmojiUnicodeCategory(rawEmoji.category),
|
||||
pickerCategory: toEmojiPickerCategory(rawEmoji.category),
|
||||
defaultVariant: defaultVariant.key,
|
||||
defaultSkinToneVariants,
|
||||
englishShortNameDefault: rawEmoji.short_name as EmojiEnglishShortName,
|
||||
englishShortNames: rawEmoji.short_names as Array<EmojiEnglishShortName>,
|
||||
emoticonDefault: rawEmoji.text ?? null,
|
||||
emoticons: rawEmoji.texts ?? [],
|
||||
};
|
||||
|
||||
addParent(parent, rawEmoji.sort_order);
|
||||
}
|
||||
|
||||
export function getEmojiDebugLabel(input: string): string {
|
||||
return Array.from(input.slice(0, 12), char => {
|
||||
const num = char.codePointAt(0) ?? 0;
|
||||
const hex = num.toString(16).toUpperCase().padStart(4, '0');
|
||||
return `U+${hex}`;
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
export function isEmojiParentValueDeprecated(input: EmojiParentValue): boolean {
|
||||
return UNICODE_DEPRECATED_EMOJI.has(input);
|
||||
}
|
||||
|
||||
export function isEmojiVariantKey(input: string): input is EmojiVariantKey {
|
||||
return EMOJI_INDEX.variantByKey.has(input as EmojiVariantKey);
|
||||
}
|
||||
|
||||
export function isEmojiParentValue(input: string): input is EmojiParentValue {
|
||||
return EMOJI_INDEX.parentKeysByValue.has(input as EmojiParentValue);
|
||||
}
|
||||
|
||||
export function isEmojiVariantValue(input: string): input is EmojiVariantValue {
|
||||
return EMOJI_INDEX.variantKeysByValue.has(input as EmojiVariantValue);
|
||||
}
|
||||
|
||||
export function isEmojiVariantValueNonQualified(
|
||||
input: EmojiVariantValue
|
||||
): boolean {
|
||||
return EMOJI_INDEX.variantKeysByValueNonQualified.has(input);
|
||||
}
|
||||
|
||||
/** @deprecated Prefer EmojiKey for refs, load short names from translations */
|
||||
export function isEmojiEnglishShortName(
|
||||
input: string
|
||||
): input is EmojiEnglishShortName {
|
||||
return EMOJI_INDEX.parentKeysByName.has(input as EmojiEnglishShortName);
|
||||
}
|
||||
|
||||
export function getEmojiParentByKey(key: EmojiParentKey): EmojiParentData {
|
||||
const data = EMOJI_INDEX.parentByKey.get(key);
|
||||
strictAssert(data, `Missing emoji parent data for key "${key}"`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getEmojiVariantByKey(key: EmojiVariantKey): EmojiVariantData {
|
||||
const data = EMOJI_INDEX.variantByKey.get(key);
|
||||
strictAssert(data, `Missing emoji variant data for key "${key}"`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getEmojiParentKeyByValue(
|
||||
value: EmojiParentValue
|
||||
): EmojiParentKey {
|
||||
const key = EMOJI_INDEX.parentKeysByValue.get(value);
|
||||
strictAssert(key, `Missing emoji parent key for value "${value}"`);
|
||||
return key;
|
||||
}
|
||||
|
||||
export function getEmojiVariantKeyByValue(
|
||||
value: EmojiVariantValue
|
||||
): EmojiVariantKey {
|
||||
const key = EMOJI_INDEX.variantKeysByValue.get(value);
|
||||
strictAssert(key, `Missing emoji variant key for value "${value}"`);
|
||||
return key;
|
||||
}
|
||||
|
||||
export function getEmojiParentKeyByVariantKey(
|
||||
key: EmojiVariantKey
|
||||
): EmojiParentKey {
|
||||
const parentKey = EMOJI_INDEX.parentKeysByVariantKeys.get(key);
|
||||
strictAssert(parentKey, `Missing parent key for variant key "${key}"`);
|
||||
return parentKey;
|
||||
}
|
||||
|
||||
export function getEmojiPickerCategoryParentKeys(
|
||||
category: EmojiPickerCategory
|
||||
): ReadonlyArray<EmojiParentKey> {
|
||||
const parents = EMOJI_INDEX.pickerCategories[category];
|
||||
strictAssert(parents, `Missing category emojis for ${category}`);
|
||||
return parents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a skin tone (if possible) to any parent key.
|
||||
*/
|
||||
export function getEmojiVariantKeyByParentKeyAndSkinTone(
|
||||
key: EmojiParentKey,
|
||||
skinTone: EmojiSkinTone
|
||||
): EmojiVariantKey {
|
||||
const parent = getEmojiParentByKey(key);
|
||||
const skinToneVariants = parent.defaultSkinToneVariants;
|
||||
|
||||
if (skinTone === EmojiSkinTone.None || skinToneVariants == null) {
|
||||
return parent.defaultVariant;
|
||||
}
|
||||
|
||||
const variantKey = skinToneVariants[skinTone];
|
||||
strictAssert(variantKey, `Missing skin tone variant for ${skinTone}`);
|
||||
|
||||
return variantKey;
|
||||
}
|
||||
|
||||
export function getEmojiVariantByParentKeyAndSkinTone(
|
||||
key: EmojiParentKey,
|
||||
skinTone: EmojiSkinTone
|
||||
): EmojiVariantData {
|
||||
return getEmojiVariantByKey(
|
||||
getEmojiVariantKeyByParentKeyAndSkinTone(key, skinTone)
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export function getEmojiParentKeyByEnglishShortName(
|
||||
englishShortName: EmojiEnglishShortName
|
||||
): EmojiParentKey {
|
||||
const emojiKey = EMOJI_INDEX.parentKeysByName.get(englishShortName);
|
||||
strictAssert(emojiKey, `Missing emoji info for ${englishShortName}`);
|
||||
return emojiKey;
|
||||
}
|
||||
|
||||
export function getEmojiDefaultEnglishSearchIndex(): FunEmojiSearchIndex {
|
||||
return EMOJI_INDEX.defaultEnglishSearchIndex;
|
||||
}
|
||||
|
||||
export function getEmojiDefaultEnglishLocalizerIndex(): FunEmojiLocalizerIndex {
|
||||
return EMOJI_INDEX.defaultEnglishLocalizerIndex;
|
||||
}
|
||||
|
||||
/** @testexport */
|
||||
export function _getAllEmojiVariantKeys(): Iterable<EmojiVariantKey> {
|
||||
return EMOJI_INDEX.variantByKey.keys();
|
||||
}
|
||||
|
||||
function emojiParentKeyConstant(input: string): EmojiParentKey {
|
||||
strictAssert(
|
||||
isEmojiParentValue(input),
|
||||
`Missing emoji parent for value "${input}"`
|
||||
);
|
||||
return getEmojiParentKeyByValue(input);
|
||||
}
|
||||
|
||||
function emojiVariantKeyConstant(input: string): EmojiVariantKey {
|
||||
strictAssert(
|
||||
isEmojiVariantValue(input),
|
||||
`Missing emoji variant for value "${input}"`
|
||||
);
|
||||
return getEmojiVariantKeyByValue(input);
|
||||
}
|
||||
|
||||
export const EMOJI_PARENT_KEY_CONSTANTS = {
|
||||
RED_HEART: emojiParentKeyConstant('\u{2764}\u{FE0F}'),
|
||||
CRYING_FACE: emojiParentKeyConstant('\u{1F622}'),
|
||||
FACE_WITH_TEARS_OF_JOY: emojiParentKeyConstant('\u{1F602}'),
|
||||
FACE_WITH_OPEN_MOUTH: emojiParentKeyConstant('\u{1F62E}'),
|
||||
ENRAGED_FACE: emojiParentKeyConstant('\u{1F621}'),
|
||||
SLIGHTLY_SMILING_FACE: emojiParentKeyConstant('\u{1F642}'),
|
||||
SLIGHTLY_FROWNING_FACE: emojiParentKeyConstant('\u{1F641}'),
|
||||
GRINNING_FACE: emojiParentKeyConstant('\u{1F600}'),
|
||||
FACE_BLOWING_A_KISS: emojiParentKeyConstant('\u{1F618}'),
|
||||
FACE_WITH_STUCK_OUT_TONGUE: emojiParentKeyConstant('\u{1F61B}'),
|
||||
CONFUSED_FACE: emojiParentKeyConstant('\u{1F615}'),
|
||||
NEUTRAL_FACE: emojiParentKeyConstant('\u{1F610}'),
|
||||
WINKING_FACE: emojiParentKeyConstant('\u{1F609}'),
|
||||
ZIPPER_MOUTH_FACE: emojiParentKeyConstant('\u{1F910}'),
|
||||
THUMBS_UP: emojiParentKeyConstant('\u{1F44D}'),
|
||||
THUMBS_DOWN: emojiParentKeyConstant('\u{1F44E}'),
|
||||
RAISED_HAND: emojiParentKeyConstant('\u{270B}'),
|
||||
WAVING_HAND: emojiParentKeyConstant('\u{1F44B}'),
|
||||
HOT_BEVERAGE: emojiParentKeyConstant('\u{2615}'),
|
||||
MOBILE_PHONE_OFF: emojiParentKeyConstant('\u{1F4F4}'),
|
||||
} as const;
|
||||
|
||||
export const EMOJI_VARIANT_KEY_CONSTANTS = {
|
||||
SLIGHTLY_FROWNING_FACE: emojiVariantKeyConstant('\u{1F641}'),
|
||||
GRINNING_FACE_WITH_SMILING_EYES: emojiVariantKeyConstant('\u{1F604}'),
|
||||
GRINNING_CAT_WITH_SMILING_EYES: emojiVariantKeyConstant('\u{1F638}'),
|
||||
FRIED_SHRIMP: emojiVariantKeyConstant('\u{1F364}'),
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Completions
|
||||
*/
|
||||
|
||||
/** For displaying in the ui */
|
||||
export function normalizeShortNameCompletionDisplay(shortName: string): string {
|
||||
return shortName
|
||||
.normalize('NFD')
|
||||
.replaceAll(/[\s,]+/gi, '_')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/** For matching in search utils */
|
||||
export function normalizeShortNameCompletionQuery(query: string): string {
|
||||
return removeDiacritics(query)
|
||||
.normalize('NFD')
|
||||
.replaceAll(/(?<!^)[\s,_-]+/gi, ' ')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Emojify
|
||||
*/
|
||||
|
||||
export function isSafeEmojifyEmoji(value: string): value is EmojiVariantValue {
|
||||
return isEmojiVariantValue(value) && !isEmojiVariantValueNonQualified(value);
|
||||
}
|
||||
|
||||
export type EmojifyData = Readonly<{
|
||||
text: string;
|
||||
emojiCount: number;
|
||||
isEmojiOnlyText: boolean;
|
||||
}>;
|
||||
|
||||
export function getEmojifyData(input: string): EmojifyData {
|
||||
// Fast path, and treat empty strings like they have non-emoji text
|
||||
if (input === '' || input.trim() === '') {
|
||||
return { text: input, emojiCount: 0, isEmojiOnlyText: false };
|
||||
}
|
||||
|
||||
let hasEmojis = false;
|
||||
let hasNonEmojis = false;
|
||||
let emojiCount = 0;
|
||||
|
||||
const regex = emojiRegex();
|
||||
|
||||
let match = regex.exec(input);
|
||||
let lastIndex = 0;
|
||||
while (match) {
|
||||
const value = match[0];
|
||||
|
||||
// Only consider safe emojis as matches
|
||||
if (!isSafeEmojifyEmoji(value)) {
|
||||
log.warn(
|
||||
`Expected a valid emoji variant value, got ${getEmojiDebugLabel(value)}`
|
||||
);
|
||||
} else {
|
||||
const { index } = match;
|
||||
hasEmojis = true;
|
||||
// Track if we skipped over any text
|
||||
if (index > lastIndex) {
|
||||
hasNonEmojis = true;
|
||||
lastIndex += index;
|
||||
}
|
||||
emojiCount += 1;
|
||||
// Needs to be the value.length not the match.length
|
||||
lastIndex = index + value.length;
|
||||
}
|
||||
|
||||
match = regex.exec(input);
|
||||
}
|
||||
|
||||
// Track if we had any remaining text
|
||||
if (lastIndex === 0 || lastIndex < input.length) {
|
||||
hasNonEmojis = true;
|
||||
}
|
||||
|
||||
return {
|
||||
text: input,
|
||||
emojiCount,
|
||||
isEmojiOnlyText: hasEmojis && !hasNonEmojis,
|
||||
};
|
||||
}
|
||||
@ -48,21 +48,6 @@ import {
|
||||
FunSubNavListBox,
|
||||
FunSubNavListBoxItem,
|
||||
} from '../base/FunSubNav.dom.tsx';
|
||||
import type { EmojiVariantKey } from '../data/emojis.std.ts';
|
||||
import {
|
||||
EmojiSkinTone,
|
||||
EmojiPickerCategory,
|
||||
getEmojiParentByKey,
|
||||
getEmojiPickerCategoryParentKeys,
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
normalizeShortNameCompletionDisplay,
|
||||
isEmojiVariantKey,
|
||||
getEmojiParentKeyByVariantKey,
|
||||
getEmojiVariantByKey,
|
||||
EMOJI_PARENT_KEY_CONSTANTS,
|
||||
EMOJI_VARIANT_KEY_CONSTANTS,
|
||||
} from '../data/emojis.std.ts';
|
||||
import { useFunEmojiSearch } from '../useFunEmojiSearch.dom.tsx';
|
||||
import { FunKeyboard } from '../keyboard/FunKeyboard.dom.tsx';
|
||||
import type { GridKeyboardState } from '../keyboard/GridKeyboardDelegate.dom.tsx';
|
||||
import { GridKeyboardDelegate } from '../keyboard/GridKeyboardDelegate.dom.tsx';
|
||||
@ -76,8 +61,8 @@ import { FunSkinTonesList } from '../FunSkinTones.dom.tsx';
|
||||
import { FunStaticEmoji } from '../FunEmoji.dom.tsx';
|
||||
import { useFunContext } from '../FunProvider.dom.tsx';
|
||||
import { FunResults, FunResultsHeader } from '../base/FunResults.dom.tsx';
|
||||
import { useFunEmojiLocalizer } from '../useFunEmojiLocalizer.dom.tsx';
|
||||
import { FunTooltip } from '../base/FunTooltip.dom.tsx';
|
||||
import { Emoji } from '../../../axo/emoji.std.ts';
|
||||
|
||||
function getTitleForSection(
|
||||
i18n: LocalizerType,
|
||||
@ -92,28 +77,28 @@ function getTitleForSection(
|
||||
if (section === FunEmojisBase.ThisMessage) {
|
||||
return i18n('icu:FunPanelEmojis__SectionTitle--ThisMessage');
|
||||
}
|
||||
if (section === EmojiPickerCategory.SmileysAndPeople) {
|
||||
if (section === Emoji.Category.SMILIES_AND_PEOPLE) {
|
||||
return i18n('icu:FunPanelEmojis__SectionTitle--SmileysAndPeople');
|
||||
}
|
||||
if (section === EmojiPickerCategory.AnimalsAndNature) {
|
||||
if (section === Emoji.Category.ANIMALS_AND_NATURE) {
|
||||
return i18n('icu:FunPanelEmojis__SectionTitle--AnimalsAndNature');
|
||||
}
|
||||
if (section === EmojiPickerCategory.FoodAndDrink) {
|
||||
if (section === Emoji.Category.FOOD_AND_DRINK) {
|
||||
return i18n('icu:FunPanelEmojis__SectionTitle--FoodAndDrink');
|
||||
}
|
||||
if (section === EmojiPickerCategory.TravelAndPlaces) {
|
||||
if (section === Emoji.Category.TRAVEL_AND_PLACES) {
|
||||
return i18n('icu:FunPanelEmojis__SectionTitle--TravelAndPlaces');
|
||||
}
|
||||
if (section === EmojiPickerCategory.Activities) {
|
||||
if (section === Emoji.Category.ACTIVITIES) {
|
||||
return i18n('icu:FunPanelEmojis__SectionTitle--Activities');
|
||||
}
|
||||
if (section === EmojiPickerCategory.Objects) {
|
||||
if (section === Emoji.Category.OBJECTS) {
|
||||
return i18n('icu:FunPanelEmojis__SectionTitle--Objects');
|
||||
}
|
||||
if (section === EmojiPickerCategory.Symbols) {
|
||||
if (section === Emoji.Category.SYMBOLS) {
|
||||
return i18n('icu:FunPanelEmojis__SectionTitle--Symbols');
|
||||
}
|
||||
if (section === EmojiPickerCategory.Flags) {
|
||||
if (section === Emoji.Category.FLAGS) {
|
||||
return i18n('icu:FunPanelEmojis__SectionTitle--Flags');
|
||||
}
|
||||
throw missingCaseError(section);
|
||||
@ -129,7 +114,7 @@ const EMOJI_GRID_ROW_SIZE = EMOJI_GRID_CELL_HEIGHT;
|
||||
|
||||
function toGridSectionNode(
|
||||
section: FunEmojisSection,
|
||||
emojiKeys: ReadonlyArray<EmojiVariantKey>
|
||||
emojis: ReadonlyArray<Emoji.Variant>
|
||||
): GridSectionNode {
|
||||
return {
|
||||
id: section,
|
||||
@ -137,10 +122,10 @@ function toGridSectionNode(
|
||||
header: {
|
||||
key: `header-${section}`,
|
||||
},
|
||||
cells: emojiKeys.map(emojiKey => {
|
||||
cells: emojis.map(emoji => {
|
||||
return {
|
||||
key: `cell-${section}-${emojiKey}`,
|
||||
value: emojiKey,
|
||||
key: `cell-${section}-${emoji}`,
|
||||
value: emoji,
|
||||
};
|
||||
}),
|
||||
};
|
||||
@ -157,7 +142,7 @@ function getSelectedSection(
|
||||
return FunSectionCommon.Recents;
|
||||
}
|
||||
|
||||
return EmojiPickerCategory.SmileysAndPeople;
|
||||
return Emoji.Category.SMILIES_AND_PEOPLE;
|
||||
}
|
||||
|
||||
function isKeyboardPointerEvent(event: PointerEvent): boolean {
|
||||
@ -167,7 +152,7 @@ function isKeyboardPointerEvent(event: PointerEvent): boolean {
|
||||
}
|
||||
|
||||
export type FunEmojiSelection = Readonly<{
|
||||
variantKey: EmojiVariantKey;
|
||||
emoji: Emoji.Variant;
|
||||
}>;
|
||||
|
||||
export type FunPanelEmojisProps = Readonly<{
|
||||
@ -175,7 +160,7 @@ export type FunPanelEmojisProps = Readonly<{
|
||||
onClose: () => void;
|
||||
showCustomizePreferredReactionsButton: boolean;
|
||||
closeOnSelect: boolean;
|
||||
messageEmojis?: ReadonlyArray<EmojiVariantKey>;
|
||||
messageEmojis?: ReadonlyArray<Emoji.Variant>;
|
||||
}>;
|
||||
|
||||
export function FunPanelEmojis({
|
||||
@ -213,20 +198,15 @@ export function FunPanelEmojis({
|
||||
return getSelectedSection(hasSearchQuery, hasRecentEmojis);
|
||||
});
|
||||
|
||||
const searchEmojis = useFunEmojiSearch();
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const skinTone = fun.emojiSkinToneDefault ?? EmojiSkinTone.None;
|
||||
const skinTone = fun.emojiSkinToneDefault ?? Emoji.SkinTone.None;
|
||||
|
||||
if (searchQuery !== '') {
|
||||
return [
|
||||
toGridSectionNode(
|
||||
FunSectionCommon.SearchResults,
|
||||
searchEmojis(searchQuery).map(result => {
|
||||
return getEmojiVariantByParentKeyAndSkinTone(
|
||||
result.parentKey,
|
||||
skinTone
|
||||
).key;
|
||||
Emoji.search(searchQuery).map(result => {
|
||||
return Emoji.getVariant(result, skinTone);
|
||||
})
|
||||
),
|
||||
];
|
||||
@ -248,37 +228,27 @@ export function FunPanelEmojis({
|
||||
result.push(
|
||||
toGridSectionNode(
|
||||
FunSectionCommon.Recents,
|
||||
recentEmojis.map(parentKey => {
|
||||
return getEmojiVariantByParentKeyAndSkinTone(
|
||||
parentKey,
|
||||
skinTone
|
||||
).key;
|
||||
recentEmojis.map(parent => {
|
||||
return Emoji.getVariant(parent, skinTone);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const emojiKeys = getEmojiPickerCategoryParentKeys(section);
|
||||
const emojis = Emoji.getCategoryParents(section);
|
||||
result.push(
|
||||
toGridSectionNode(
|
||||
section,
|
||||
emojiKeys.map(parentKey => {
|
||||
return getEmojiVariantByParentKeyAndSkinTone(parentKey, skinTone)
|
||||
.key;
|
||||
emojis.map(parent => {
|
||||
return Emoji.getVariant(parent, skinTone);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [
|
||||
fun.emojiSkinToneDefault,
|
||||
searchQuery,
|
||||
searchEmojis,
|
||||
messageEmojis,
|
||||
recentEmojis,
|
||||
]);
|
||||
}, [fun.emojiSkinToneDefault, searchQuery, messageEmojis, recentEmojis]);
|
||||
|
||||
const [virtualizer, layout] = useFunVirtualGrid({
|
||||
scrollerRef,
|
||||
@ -406,7 +376,7 @@ export function FunPanelEmojis({
|
||||
</FunSubNavListBoxItem>
|
||||
)}
|
||||
<FunSubNavListBoxItem
|
||||
id={EmojiPickerCategory.SmileysAndPeople}
|
||||
id={Emoji.Category.SMILIES_AND_PEOPLE}
|
||||
label={i18n(
|
||||
'icu:FunPanelEmojis__SubNavCategoryLabel--SmileysAndPeople'
|
||||
)}
|
||||
@ -414,7 +384,7 @@ export function FunPanelEmojis({
|
||||
<FunSubNavIcon iconClassName="FunSubNav__Icon--SmileysAndPeople" />
|
||||
</FunSubNavListBoxItem>
|
||||
<FunSubNavListBoxItem
|
||||
id={EmojiPickerCategory.AnimalsAndNature}
|
||||
id={Emoji.Category.ANIMALS_AND_NATURE}
|
||||
label={i18n(
|
||||
'icu:FunPanelEmojis__SubNavCategoryLabel--AnimalsAndNature'
|
||||
)}
|
||||
@ -422,7 +392,7 @@ export function FunPanelEmojis({
|
||||
<FunSubNavIcon iconClassName="FunSubNav__Icon--AnimalsAndNature" />
|
||||
</FunSubNavListBoxItem>
|
||||
<FunSubNavListBoxItem
|
||||
id={EmojiPickerCategory.FoodAndDrink}
|
||||
id={Emoji.Category.FOOD_AND_DRINK}
|
||||
label={i18n(
|
||||
'icu:FunPanelEmojis__SubNavCategoryLabel--FoodAndDrink'
|
||||
)}
|
||||
@ -430,7 +400,7 @@ export function FunPanelEmojis({
|
||||
<FunSubNavIcon iconClassName="FunSubNav__Icon--FoodAndDrink" />
|
||||
</FunSubNavListBoxItem>
|
||||
<FunSubNavListBoxItem
|
||||
id={EmojiPickerCategory.Activities}
|
||||
id={Emoji.Category.ACTIVITIES}
|
||||
label={i18n(
|
||||
'icu:FunPanelEmojis__SubNavCategoryLabel--Activities'
|
||||
)}
|
||||
@ -438,7 +408,7 @@ export function FunPanelEmojis({
|
||||
<FunSubNavIcon iconClassName="FunSubNav__Icon--Activities" />
|
||||
</FunSubNavListBoxItem>
|
||||
<FunSubNavListBoxItem
|
||||
id={EmojiPickerCategory.TravelAndPlaces}
|
||||
id={Emoji.Category.TRAVEL_AND_PLACES}
|
||||
label={i18n(
|
||||
'icu:FunPanelEmojis__SubNavCategoryLabel--TravelAndPlaces'
|
||||
)}
|
||||
@ -446,19 +416,19 @@ export function FunPanelEmojis({
|
||||
<FunSubNavIcon iconClassName="FunSubNav__Icon--TravelAndPlaces" />
|
||||
</FunSubNavListBoxItem>
|
||||
<FunSubNavListBoxItem
|
||||
id={EmojiPickerCategory.Objects}
|
||||
id={Emoji.Category.OBJECTS}
|
||||
label={i18n('icu:FunPanelEmojis__SubNavCategoryLabel--Objects')}
|
||||
>
|
||||
<FunSubNavIcon iconClassName="FunSubNav__Icon--Objects" />
|
||||
</FunSubNavListBoxItem>
|
||||
<FunSubNavListBoxItem
|
||||
id={EmojiPickerCategory.Symbols}
|
||||
id={Emoji.Category.SYMBOLS}
|
||||
label={i18n('icu:FunPanelEmojis__SubNavCategoryLabel--Symbols')}
|
||||
>
|
||||
<FunSubNavIcon iconClassName="FunSubNav__Icon--Symbols" />
|
||||
</FunSubNavListBoxItem>
|
||||
<FunSubNavListBoxItem
|
||||
id={EmojiPickerCategory.Flags}
|
||||
id={Emoji.Category.FLAGS}
|
||||
label={i18n('icu:FunPanelEmojis__SubNavCategoryLabel--Flags')}
|
||||
>
|
||||
<FunSubNavIcon iconClassName="FunSubNav__Icon--Flags" />
|
||||
@ -481,9 +451,7 @@ export function FunPanelEmojis({
|
||||
<FunStaticEmoji
|
||||
size={16}
|
||||
role="presentation"
|
||||
emoji={getEmojiVariantByKey(
|
||||
EMOJI_VARIANT_KEY_CONSTANTS.SLIGHTLY_FROWNING_FACE
|
||||
)}
|
||||
emoji={Emoji.SLIGHTLY_FROWNING_FACE}
|
||||
/>
|
||||
</FunResultsHeader>
|
||||
</FunResults>
|
||||
@ -519,8 +487,7 @@ export function FunPanelEmojis({
|
||||
section.id as FunEmojisSection
|
||||
)}
|
||||
</FunGridHeaderText>
|
||||
{section.id ===
|
||||
EmojiPickerCategory.SmileysAndPeople && (
|
||||
{section.id === Emoji.Category.SMILIES_AND_PEOPLE && (
|
||||
<SectionSkinToneHeaderPopover
|
||||
i18n={i18n}
|
||||
open={skinTonePopoverOpen}
|
||||
@ -573,12 +540,12 @@ type RowProps = Readonly<{
|
||||
rowIndex: number;
|
||||
cells: ReadonlyArray<CellLayoutNode>;
|
||||
focusedCellKey: CellKey | null;
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
emojiSkinToneDefault: Emoji.SkinTone | null;
|
||||
onSelectEmoji: (
|
||||
emojiSelection: FunEmojiSelection,
|
||||
shouldClose: boolean
|
||||
) => void;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinTone: Emoji.SkinTone) => void;
|
||||
}>;
|
||||
|
||||
const Row = memo(function Row(props: RowProps): JSX.Element {
|
||||
@ -593,7 +560,7 @@ const Row = memo(function Row(props: RowProps): JSX.Element {
|
||||
<Cell
|
||||
key={cell.key}
|
||||
i18n={props.i18n}
|
||||
value={cell.value}
|
||||
emoji={Emoji.unsafeCastMaybeInvalidStringToVariant(cell.value)}
|
||||
cellKey={cell.key}
|
||||
rowIndex={cell.rowIndex}
|
||||
colIndex={cell.colIndex}
|
||||
@ -610,27 +577,27 @@ const Row = memo(function Row(props: RowProps): JSX.Element {
|
||||
|
||||
type CellProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
value: string;
|
||||
emoji: Emoji.Variant;
|
||||
cellKey: CellKey;
|
||||
colIndex: number;
|
||||
rowIndex: number;
|
||||
isTabbable: boolean;
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
emojiSkinToneDefault: Emoji.SkinTone | null;
|
||||
onSelectEmoji: (
|
||||
emojiSelection: FunEmojiSelection,
|
||||
shouldClose: boolean
|
||||
) => void;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void;
|
||||
onEmojiSkinToneDefaultChange: (emojiSkinTone: Emoji.SkinTone) => void;
|
||||
}>;
|
||||
|
||||
const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
const {
|
||||
i18n,
|
||||
emoji,
|
||||
emojiSkinToneDefault,
|
||||
onSelectEmoji,
|
||||
onEmojiSkinToneDefaultChange,
|
||||
} = props;
|
||||
const emojiLocalizer = useFunEmojiLocalizer();
|
||||
|
||||
const popoverTriggerRef = useRef<HTMLButtonElement>(null);
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
@ -639,26 +606,12 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
}, []);
|
||||
|
||||
const emojiParent = useMemo(() => {
|
||||
const isVariantKey = isEmojiVariantKey(props.value);
|
||||
|
||||
strictAssert(isVariantKey, 'Cell value is not a variant key');
|
||||
|
||||
const parentKey = getEmojiParentKeyByVariantKey(props.value);
|
||||
|
||||
return getEmojiParentByKey(parentKey);
|
||||
}, [props.value]);
|
||||
return Emoji.getParent(emoji);
|
||||
}, [emoji]);
|
||||
|
||||
const emojiHasSkinToneVariants = useMemo(() => {
|
||||
return emojiParent.defaultSkinToneVariants != null;
|
||||
}, [emojiParent.defaultSkinToneVariants]);
|
||||
|
||||
const emojiVariant = useMemo(() => {
|
||||
const isVariantKey = isEmojiVariantKey(props.value);
|
||||
|
||||
strictAssert(isVariantKey, 'Cell value is not a variant key');
|
||||
|
||||
return getEmojiVariantByKey(props.value);
|
||||
}, [props.value]);
|
||||
return Emoji.hasSkinToneVariants(emojiParent);
|
||||
}, [emojiParent]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event: PointerEvent) => {
|
||||
@ -667,18 +620,13 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
return;
|
||||
}
|
||||
const emojiSelection: FunEmojiSelection = {
|
||||
variantKey: emojiVariant.key,
|
||||
emoji,
|
||||
};
|
||||
const shouldClose =
|
||||
isKeyboardPointerEvent(event) && !(event.ctrlKey || event.metaKey);
|
||||
onSelectEmoji(emojiSelection, shouldClose);
|
||||
},
|
||||
[
|
||||
emojiHasSkinToneVariants,
|
||||
emojiSkinToneDefault,
|
||||
emojiVariant.key,
|
||||
onSelectEmoji,
|
||||
]
|
||||
[emojiHasSkinToneVariants, emojiSkinToneDefault, emoji, onSelectEmoji]
|
||||
);
|
||||
|
||||
const handleLongPress = useCallback(() => {
|
||||
@ -699,28 +647,25 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
);
|
||||
|
||||
const handleSelectSkinTone = useCallback(
|
||||
(skinToneSelection: EmojiSkinTone) => {
|
||||
const variant = getEmojiVariantByParentKeyAndSkinTone(
|
||||
emojiParent.key,
|
||||
skinToneSelection
|
||||
);
|
||||
(skinToneSelection: Emoji.SkinTone) => {
|
||||
const variant = Emoji.getVariant(emojiParent, skinToneSelection);
|
||||
onEmojiSkinToneDefaultChange(skinToneSelection);
|
||||
const emojiSelection: FunEmojiSelection = {
|
||||
variantKey: variant.key,
|
||||
emoji: variant,
|
||||
};
|
||||
const shouldClose = true;
|
||||
onSelectEmoji(emojiSelection, shouldClose);
|
||||
},
|
||||
[onEmojiSkinToneDefaultChange, emojiParent.key, onSelectEmoji]
|
||||
[onEmojiSkinToneDefaultChange, emojiParent, onSelectEmoji]
|
||||
);
|
||||
|
||||
const emojiName = useMemo(() => {
|
||||
return emojiLocalizer.getLocaleShortName(emojiVariant.key);
|
||||
}, [emojiVariant.key, emojiLocalizer]);
|
||||
const emojiDisplayLabel = useMemo(() => {
|
||||
return Emoji.getDisplayLabel(emoji);
|
||||
}, [emoji]);
|
||||
|
||||
const emojiShortNameDisplay = useMemo(() => {
|
||||
return normalizeShortNameCompletionDisplay(emojiName);
|
||||
}, [emojiName]);
|
||||
const emojiCompletionLabel = useMemo(() => {
|
||||
return Emoji.getCompletionLabel(emoji);
|
||||
}, [emoji]);
|
||||
|
||||
return (
|
||||
<FunGridCell
|
||||
@ -730,7 +675,7 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
>
|
||||
<FunTooltip
|
||||
side="top"
|
||||
content={`:${emojiShortNameDisplay}:`}
|
||||
content={`:${emojiCompletionLabel}:`}
|
||||
collisionBoundarySelector=".FunScroller__Viewport"
|
||||
collisionPadding={6}
|
||||
// `skipDelayDuration=0` doesn't work with `disableHoverableContent`
|
||||
@ -740,7 +685,7 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
<FunItemButton
|
||||
ref={popoverTriggerRef}
|
||||
excludeFromTabOrder={!props.isTabbable}
|
||||
aria-label={emojiName}
|
||||
aria-label={emojiDisplayLabel}
|
||||
onClick={handleClick}
|
||||
onLongPress={handleLongPress}
|
||||
onContextMenu={handleContextMenu}
|
||||
@ -748,7 +693,7 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
'icu:FunPanelEmojis__SkinTonePicker__LongPressAccessibilityDescription'
|
||||
)}
|
||||
>
|
||||
<FunStaticEmoji role="presentation" size={32} emoji={emojiVariant} />
|
||||
<FunStaticEmoji role="presentation" size={32} emoji={emoji} />
|
||||
</FunItemButton>
|
||||
</FunTooltip>
|
||||
{emojiHasSkinToneVariants && (
|
||||
@ -771,13 +716,13 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||
<Heading slot="title">
|
||||
{i18n(
|
||||
'icu:FunPanelEmojis__SkinTonePicker__SelectSkinToneForSelectedEmoji',
|
||||
{ emojiName }
|
||||
{ emojiName: emojiDisplayLabel }
|
||||
)}
|
||||
</Heading>
|
||||
</VisuallyHidden>
|
||||
<FunSkinTonesList
|
||||
i18n={i18n}
|
||||
emoji={emojiParent.key}
|
||||
emoji={emojiParent}
|
||||
skinTone={null}
|
||||
onSelectSkinTone={handleSelectSkinTone}
|
||||
/>
|
||||
@ -792,7 +737,7 @@ type SectionSkinToneHeaderPopoverProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSelectSkinTone: (emojiSkinTone: EmojiSkinTone) => void;
|
||||
onSelectSkinTone: (emojiSkinTone: Emoji.SkinTone) => void;
|
||||
}>;
|
||||
|
||||
function SectionSkinToneHeaderPopover(
|
||||
@ -801,7 +746,7 @@ function SectionSkinToneHeaderPopover(
|
||||
const { i18n, onOpenChange, onSelectSkinTone } = props;
|
||||
|
||||
const handleSelectSkinTone = useCallback(
|
||||
(emojiSkinTone: EmojiSkinTone) => {
|
||||
(emojiSkinTone: Emoji.SkinTone) => {
|
||||
onSelectSkinTone(emojiSkinTone);
|
||||
onOpenChange(false);
|
||||
},
|
||||
@ -821,7 +766,7 @@ function SectionSkinToneHeaderPopover(
|
||||
</FunGridHeaderPopoverHeader>
|
||||
<FunSkinTonesList
|
||||
i18n={i18n}
|
||||
emoji={EMOJI_PARENT_KEY_CONSTANTS.RAISED_HAND}
|
||||
emoji={Emoji.HAND}
|
||||
skinTone={null}
|
||||
onSelectSkinTone={handleSelectSkinTone}
|
||||
/>
|
||||
|
||||
@ -64,10 +64,7 @@ import type { LocalizerType } from '../../../types/I18N.std.ts';
|
||||
import { isAbortError } from '../../../util/isAbortError.std.ts';
|
||||
import { createLogger } from '../../../logging/log.std.ts';
|
||||
import * as Errors from '../../../types/errors.std.ts';
|
||||
import {
|
||||
EMOJI_VARIANT_KEY_CONSTANTS,
|
||||
getEmojiVariantByKey,
|
||||
} from '../data/emojis.std.ts';
|
||||
import { Emoji } from '../../../axo/emoji.std.ts';
|
||||
import type { fetchGiphyFile } from '../../../state/smart/fun/giphy.preload.ts';
|
||||
import {
|
||||
getGifCdnUrlOrigin,
|
||||
@ -561,9 +558,7 @@ export function FunPanelGifs({
|
||||
<FunStaticEmoji
|
||||
size={16}
|
||||
role="presentation"
|
||||
emoji={getEmojiVariantByKey(
|
||||
EMOJI_VARIANT_KEY_CONSTANTS.SLIGHTLY_FROWNING_FACE
|
||||
)}
|
||||
emoji={Emoji.SLIGHTLY_FROWNING_FACE}
|
||||
/>
|
||||
</FunResultsHeader>
|
||||
)}
|
||||
|
||||
@ -48,13 +48,6 @@ import {
|
||||
FunSubNavListBoxItem,
|
||||
FunSubNavScroller,
|
||||
} from '../base/FunSubNav.dom.tsx';
|
||||
import {
|
||||
EMOJI_VARIANT_KEY_CONSTANTS,
|
||||
type EmojiParentKey,
|
||||
getEmojiParentKeyByValue,
|
||||
getEmojiVariantByKey,
|
||||
isEmojiParentValue,
|
||||
} from '../data/emojis.std.ts';
|
||||
import { FunKeyboard } from '../keyboard/FunKeyboard.dom.tsx';
|
||||
import type { GridKeyboardState } from '../keyboard/GridKeyboardDelegate.dom.tsx';
|
||||
import { GridKeyboardDelegate } from '../keyboard/GridKeyboardDelegate.dom.tsx';
|
||||
@ -77,7 +70,7 @@ import {
|
||||
import { FunSticker } from '../FunSticker.dom.tsx';
|
||||
import { getAnalogTime } from '../../../util/getAnalogTime.std.ts';
|
||||
import { getDateTimeFormatter } from '../../../util/formatTimestamp.dom.ts';
|
||||
import { useFunEmojiSearch } from '../useFunEmojiSearch.dom.tsx';
|
||||
import { Emoji } from '../../../axo/emoji.std.ts';
|
||||
|
||||
const STICKER_GRID_COLUMNS = 4;
|
||||
const STICKER_GRID_CELL_WIDTH = 80;
|
||||
@ -246,26 +239,19 @@ export function FunPanelStickers({
|
||||
);
|
||||
});
|
||||
|
||||
const searchEmojis = useFunEmojiSearch();
|
||||
|
||||
const sections = useMemo(() => {
|
||||
if (searchQuery !== '') {
|
||||
const emojiKeys = new Set<EmojiParentKey>();
|
||||
|
||||
for (const result of searchEmojis(searchQuery)) {
|
||||
emojiKeys.add(result.parentKey);
|
||||
}
|
||||
const emojis = new Set<Emoji.Parent>(Emoji.search(searchQuery));
|
||||
|
||||
const allStickers = installedStickerPacks.flatMap(pack => pack.stickers);
|
||||
const matchingStickers = allStickers.filter(sticker => {
|
||||
if (sticker.emoji == null) {
|
||||
return false;
|
||||
}
|
||||
if (!isEmojiParentValue(sticker.emoji)) {
|
||||
if (!Emoji.isParent(sticker.emoji)) {
|
||||
return false;
|
||||
}
|
||||
const parentKey = getEmojiParentKeyByValue(sticker.emoji);
|
||||
return emojiKeys.has(parentKey);
|
||||
return emojis.has(sticker.emoji);
|
||||
});
|
||||
|
||||
return [
|
||||
@ -304,13 +290,7 @@ export function FunPanelStickers({
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [
|
||||
showTimeStickers,
|
||||
recentStickers,
|
||||
installedStickerPacks,
|
||||
searchEmojis,
|
||||
searchQuery,
|
||||
]);
|
||||
}, [showTimeStickers, recentStickers, installedStickerPacks, searchQuery]);
|
||||
|
||||
const [virtualizer, layout] = useFunVirtualGrid({
|
||||
scrollerRef,
|
||||
@ -480,9 +460,7 @@ export function FunPanelStickers({
|
||||
<FunStaticEmoji
|
||||
size={16}
|
||||
role="presentation"
|
||||
emoji={getEmojiVariantByKey(
|
||||
EMOJI_VARIANT_KEY_CONSTANTS.SLIGHTLY_FROWNING_FACE
|
||||
)}
|
||||
emoji={Emoji.SLIGHTLY_FROWNING_FACE}
|
||||
/>
|
||||
</FunResultsHeader>
|
||||
</FunResults>
|
||||
|
||||
@ -1,92 +0,0 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { useMemo } from 'react';
|
||||
import type { EmojiParentKey, EmojiVariantKey } from './data/emojis.std.ts';
|
||||
import {
|
||||
getEmojiParentByKey,
|
||||
getEmojiParentKeyByVariantKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
isEmojiVariantValue,
|
||||
} from './data/emojis.std.ts';
|
||||
import type { LocaleEmojiListType } from '../../types/emoji.std.ts';
|
||||
import { useFunEmojiLocalization } from './FunEmojiLocalizationProvider.dom.tsx';
|
||||
|
||||
export type FunEmojiLocalizerIndex = Readonly<{
|
||||
parentKeyToLocaleShortName: ReadonlyMap<EmojiParentKey, string>;
|
||||
localeShortNameToParentKey: ReadonlyMap<string, EmojiParentKey>;
|
||||
}>;
|
||||
|
||||
export type FunEmojiLocalizer = Readonly<{
|
||||
getLocaleShortName: (key: EmojiVariantKey) => string;
|
||||
getParentKeyForText: (text: string) => EmojiParentKey | null;
|
||||
}>;
|
||||
|
||||
export function createFunEmojiLocalizerIndex(
|
||||
localeEmojiList: LocaleEmojiListType,
|
||||
defaultLocalizerIndex?: FunEmojiLocalizerIndex
|
||||
): FunEmojiLocalizerIndex {
|
||||
const parentKeyToLocaleShortName = new Map<EmojiParentKey, string>();
|
||||
const localeShortNameToParentKey = new Map<string, EmojiParentKey>();
|
||||
|
||||
for (const entry of localeEmojiList) {
|
||||
// Sadly some localized emoji are not present in our spritesheets
|
||||
if (!isEmojiVariantValue(entry.emoji)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const variantKey = getEmojiVariantKeyByValue(entry.emoji);
|
||||
const parentKey = getEmojiParentKeyByVariantKey(variantKey);
|
||||
const localizedShortName = entry.tags.at(0) ?? entry.shortName;
|
||||
|
||||
parentKeyToLocaleShortName.set(parentKey, localizedShortName);
|
||||
localeShortNameToParentKey.set(localizedShortName, parentKey);
|
||||
}
|
||||
|
||||
if (defaultLocalizerIndex != null) {
|
||||
for (const [
|
||||
parentKey,
|
||||
defaultShortName,
|
||||
] of defaultLocalizerIndex.parentKeyToLocaleShortName) {
|
||||
if (parentKeyToLocaleShortName.has(parentKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
parentKeyToLocaleShortName.set(parentKey, defaultShortName);
|
||||
localeShortNameToParentKey.set(defaultShortName, parentKey);
|
||||
}
|
||||
}
|
||||
|
||||
return { parentKeyToLocaleShortName, localeShortNameToParentKey };
|
||||
}
|
||||
|
||||
/** @internal exported for tests */
|
||||
export function _createFunEmojiLocalizer(
|
||||
emojiLocalizerIndex: FunEmojiLocalizerIndex
|
||||
): FunEmojiLocalizer {
|
||||
function getLocaleShortName(variantKey: EmojiVariantKey) {
|
||||
const parentKey = getEmojiParentKeyByVariantKey(variantKey);
|
||||
const localeShortName =
|
||||
emojiLocalizerIndex.parentKeyToLocaleShortName.get(parentKey);
|
||||
if (localeShortName != null) {
|
||||
return localeShortName;
|
||||
}
|
||||
// Fallback to english short name
|
||||
const parent = getEmojiParentByKey(parentKey);
|
||||
return parent.englishShortNameDefault;
|
||||
}
|
||||
|
||||
function getParentKeyForText(text: string): EmojiParentKey | null {
|
||||
const parentKey = emojiLocalizerIndex.localeShortNameToParentKey.get(text);
|
||||
return parentKey ?? null;
|
||||
}
|
||||
|
||||
return { getLocaleShortName, getParentKeyForText };
|
||||
}
|
||||
|
||||
export function useFunEmojiLocalizer(): FunEmojiLocalizer {
|
||||
const { emojiLocalizerIndex } = useFunEmojiLocalization();
|
||||
const emojiLocalizer = useMemo(() => {
|
||||
return _createFunEmojiLocalizer(emojiLocalizerIndex);
|
||||
}, [emojiLocalizerIndex]);
|
||||
return emojiLocalizer;
|
||||
}
|
||||
@ -1,157 +0,0 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import Fuse from 'fuse.js';
|
||||
import type { FuseOptionKey, IFuseOptions } from 'fuse.js';
|
||||
import lodash from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import type { EmojiParentKey } from './data/emojis.std.ts';
|
||||
import {
|
||||
getEmojiParentByKey,
|
||||
getEmojiParentKeyByValue,
|
||||
isEmojiParentValue,
|
||||
isEmojiParentValueDeprecated,
|
||||
normalizeShortNameCompletionQuery,
|
||||
} from './data/emojis.std.ts';
|
||||
import type { LocaleEmojiListType } from '../../types/emoji.std.ts';
|
||||
import { useFunEmojiLocalization } from './FunEmojiLocalizationProvider.dom.tsx';
|
||||
|
||||
const { sortBy } = lodash;
|
||||
|
||||
export type FunEmojiSearchIndexEntry = Readonly<{
|
||||
key: EmojiParentKey;
|
||||
rank: number | null;
|
||||
shortName: string;
|
||||
shortNames: ReadonlyArray<string>;
|
||||
emoticon: string | null;
|
||||
emoticons: ReadonlyArray<string>;
|
||||
}>;
|
||||
|
||||
export type FunEmojiSearchIndex = ReadonlyArray<FunEmojiSearchIndexEntry>;
|
||||
|
||||
export type FunEmojiSearchResult = Readonly<{
|
||||
parentKey: EmojiParentKey;
|
||||
}>;
|
||||
|
||||
export type FunEmojiSearch = (
|
||||
query: string,
|
||||
limit?: number
|
||||
) => ReadonlyArray<FunEmojiSearchResult>;
|
||||
|
||||
export function createFunEmojiSearchIndex(
|
||||
localeEmojiList: LocaleEmojiListType,
|
||||
defaultSearchIndex: ReadonlyArray<FunEmojiSearchIndexEntry> = []
|
||||
): FunEmojiSearchIndex {
|
||||
const results: Array<FunEmojiSearchIndexEntry> = [];
|
||||
|
||||
const localizedKeys = new Set<string>();
|
||||
|
||||
for (const localeEmoji of localeEmojiList) {
|
||||
if (!isEmojiParentValue(localeEmoji.emoji)) {
|
||||
// Skipping unknown emoji, most likely apple doesn't support it
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isEmojiParentValueDeprecated(localeEmoji.emoji)) {
|
||||
// Skipping deprecated emoji
|
||||
continue;
|
||||
}
|
||||
|
||||
const parentKey = getEmojiParentKeyByValue(localeEmoji.emoji);
|
||||
const emoji = getEmojiParentByKey(parentKey);
|
||||
localizedKeys.add(parentKey);
|
||||
results.push({
|
||||
key: parentKey,
|
||||
rank: localeEmoji.rank,
|
||||
shortName: normalizeShortNameCompletionQuery(localeEmoji.shortName),
|
||||
shortNames: localeEmoji.tags.map(tag => {
|
||||
return normalizeShortNameCompletionQuery(tag);
|
||||
}),
|
||||
emoticon: emoji.emoticonDefault,
|
||||
emoticons: emoji.emoticons,
|
||||
});
|
||||
}
|
||||
|
||||
for (const defaultEntry of defaultSearchIndex) {
|
||||
if (!localizedKeys.has(defaultEntry.key)) {
|
||||
results.push(defaultEntry);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
const FuseKeys: Array<FuseOptionKey<FunEmojiSearchIndexEntry>> = [
|
||||
{ name: 'shortName', weight: 100 },
|
||||
{ name: 'shortNames', weight: 1 },
|
||||
{ name: 'emoticon', weight: 50 },
|
||||
{ name: 'emoticons', weight: 1 },
|
||||
];
|
||||
|
||||
const FuseFuzzyOptions: IFuseOptions<FunEmojiSearchIndexEntry> = {
|
||||
shouldSort: false,
|
||||
threshold: 0.2,
|
||||
minMatchCharLength: 1,
|
||||
keys: FuseKeys,
|
||||
includeScore: true,
|
||||
includeMatches: true,
|
||||
};
|
||||
|
||||
const FuseExactOptions: IFuseOptions<FunEmojiSearchIndexEntry> = {
|
||||
shouldSort: false,
|
||||
threshold: 0,
|
||||
minMatchCharLength: 1,
|
||||
keys: FuseKeys,
|
||||
includeScore: true,
|
||||
includeMatches: true,
|
||||
};
|
||||
|
||||
/** @internal exported for tests */
|
||||
export function _createFunEmojiSearch(
|
||||
emojiSearchIndex: FunEmojiSearchIndex
|
||||
): FunEmojiSearch {
|
||||
const fuseIndex = Fuse.createIndex(FuseKeys, emojiSearchIndex);
|
||||
const fuseFuzzy = new Fuse(emojiSearchIndex, FuseFuzzyOptions, fuseIndex);
|
||||
const fuseExact = new Fuse(emojiSearchIndex, FuseExactOptions, fuseIndex);
|
||||
|
||||
return function emojiSearch(rawQuery, limit = 200) {
|
||||
const query = normalizeShortNameCompletionQuery(rawQuery);
|
||||
|
||||
// Prefer exact matches at 2 characters
|
||||
const fuse = query.length < 2 ? fuseExact : fuseFuzzy;
|
||||
|
||||
const rawResults = fuse.search(query.substring(0, 32));
|
||||
|
||||
// Note: lodash's sortBy() only calls each iteratee once
|
||||
const sortedResults = sortBy(rawResults, result => {
|
||||
const rank = result.item.rank ?? 1e9;
|
||||
|
||||
const localizedQueryMatch =
|
||||
result.item.shortNames.at(0) ?? result.item.shortName;
|
||||
|
||||
// Exact prefix matches in [0,1] range
|
||||
if (localizedQueryMatch.startsWith(query)) {
|
||||
// Note: localizedQueryMatch will always be <= in length to the query
|
||||
const matchRatio = query.length / localizedQueryMatch.length; // 1-0
|
||||
return 1 - matchRatio;
|
||||
}
|
||||
|
||||
const queryScore = result.score ?? 0; // 0-1
|
||||
const rankScore = rank / emojiSearchIndex.length; // 0-1
|
||||
|
||||
// Other matches in [1,], ordered by score and rank
|
||||
return 1 + queryScore + rankScore;
|
||||
});
|
||||
|
||||
return sortedResults.slice(0, limit).map(result => {
|
||||
return { parentKey: result.item.key };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function useFunEmojiSearch(): FunEmojiSearch {
|
||||
const { emojiSearchIndex } = useFunEmojiLocalization();
|
||||
const emojiSearch = useMemo(() => {
|
||||
return _createFunEmojiSearch(emojiSearchIndex);
|
||||
}, [emojiSearchIndex]);
|
||||
return emojiSearch;
|
||||
}
|
||||
@ -37,7 +37,6 @@ import { AxoSwitch } from '../../../axo/AxoSwitch.dom.tsx';
|
||||
import { FunEmojiPickerButton } from '../../fun/FunButton.dom.tsx';
|
||||
import { FunEmojiPicker } from '../../fun/FunEmojiPicker.dom.tsx';
|
||||
import type { FunEmojiSelection } from '../../fun/panels/FunPanelEmojis.dom.tsx';
|
||||
import { getEmojiVariantByKey } from '../../fun/data/emojis.std.ts';
|
||||
import {
|
||||
ItemAvatar,
|
||||
ItemBody,
|
||||
@ -127,9 +126,7 @@ export function PreferencesEditChatFolderPage(
|
||||
strictAssert(inputRef.current, 'Missing input ref');
|
||||
const input = inputRef.current;
|
||||
const { selectionStart, selectionEnd } = input;
|
||||
|
||||
const variant = getEmojiVariantByKey(emojiSelection.variantKey);
|
||||
const emoji = variant.value;
|
||||
const emoji = emojiSelection.emoji;
|
||||
|
||||
let newName: string;
|
||||
if (selectionStart == null || selectionEnd == null) {
|
||||
|
||||
@ -12,6 +12,11 @@ import {
|
||||
portraitTealUrl,
|
||||
squareStickerUrl,
|
||||
} from '../../storybook/Fixtures.std.ts';
|
||||
import type {
|
||||
StickerPackType,
|
||||
StickerType,
|
||||
} from '../../state/ducks/stickers.preload.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@ -21,21 +26,21 @@ export default {
|
||||
args: {},
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
const abeSticker = {
|
||||
const abeSticker: StickerType = {
|
||||
id: -1,
|
||||
emoji: '🎩',
|
||||
emoji: Emoji.TOPHAT,
|
||||
url: squareStickerUrl,
|
||||
packId: 'abe',
|
||||
};
|
||||
const wideSticker = {
|
||||
const wideSticker: StickerType = {
|
||||
id: -2,
|
||||
emoji: '🤯',
|
||||
emoji: Emoji.EXPLODING_HEAD,
|
||||
url: landscapeGreenUrl,
|
||||
packId: 'wide',
|
||||
};
|
||||
const tallSticker = {
|
||||
const tallSticker: StickerType = {
|
||||
id: -3,
|
||||
emoji: '🔥',
|
||||
emoji: Emoji.FIRE,
|
||||
url: portraitTealUrl,
|
||||
packId: 'tall',
|
||||
};
|
||||
@ -44,7 +49,7 @@ export function Full(): JSX.Element {
|
||||
const title = 'Foo';
|
||||
const author = 'Foo McBarrington';
|
||||
|
||||
const pack = {
|
||||
const pack: StickerPackType = {
|
||||
id: 'foo',
|
||||
key: 'foo',
|
||||
lastUsed: Date.now(),
|
||||
|
||||
@ -138,6 +138,7 @@ import Actions = Proto.GroupChange.Actions;
|
||||
import AccessRequired = Proto.AccessControl.AccessRequired;
|
||||
import MemberRole = Proto.Member.Role;
|
||||
import { computeGroupNameHash } from './util/Conversation.preload.ts';
|
||||
import { Emoji } from './axo/emoji.std.ts';
|
||||
|
||||
const { compact, difference, flatten, fromPairs, isNumber, omit, values } =
|
||||
lodash;
|
||||
@ -1325,7 +1326,7 @@ export function buildModifyMemberLabelChange({
|
||||
}: {
|
||||
serviceId: ServiceIdString;
|
||||
group: ConversationAttributesType;
|
||||
labelEmoji: string | undefined;
|
||||
labelEmoji: Emoji.Variant | undefined;
|
||||
labelString: string | undefined;
|
||||
}): Actions.Params {
|
||||
const logId = `buildModifyMemberLabelChange(${getConversationIdForLogging(group)})`;
|
||||
@ -6332,7 +6333,7 @@ function normalizeTimestamp(timestamp: bigint | null | undefined): number {
|
||||
|
||||
type DecryptedModifyMemberLabelAction = {
|
||||
userId: AciString;
|
||||
labelEmoji?: string;
|
||||
labelEmoji?: Emoji.Variant;
|
||||
labelString?: string;
|
||||
};
|
||||
|
||||
@ -7127,11 +7128,11 @@ function decryptModifyMemberLabelAction(
|
||||
}
|
||||
|
||||
// labelEmoji
|
||||
let decryptedLabelEmoji: string | undefined;
|
||||
let decryptedLabelEmoji: Emoji.Variant | undefined;
|
||||
if (Bytes.isNotEmpty(labelEmoji)) {
|
||||
try {
|
||||
decryptedLabelEmoji = Bytes.toString(
|
||||
decryptGroupBlob(clientZkGroupCipher, labelEmoji)
|
||||
decryptedLabelEmoji = Emoji.unsafeCastMaybeInvalidStringToVariant(
|
||||
Bytes.toString(decryptGroupBlob(clientZkGroupCipher, labelEmoji))
|
||||
);
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
@ -7364,7 +7365,7 @@ type DecryptedMember = Readonly<{
|
||||
profileKey: Uint8Array<ArrayBuffer>;
|
||||
role: MemberRole;
|
||||
joinedAtVersion: number;
|
||||
labelEmoji?: string;
|
||||
labelEmoji?: Emoji.Variant;
|
||||
labelString?: string;
|
||||
}>;
|
||||
|
||||
@ -7413,11 +7414,11 @@ function decryptMember(
|
||||
}
|
||||
|
||||
// labelEmoji
|
||||
let decryptedLabelEmoji: string | undefined;
|
||||
let decryptedLabelEmoji: Emoji.Variant | undefined;
|
||||
if (Bytes.isNotEmpty(member.labelEmoji)) {
|
||||
try {
|
||||
decryptedLabelEmoji = Bytes.toString(
|
||||
decryptGroupBlob(clientZkGroupCipher, member.labelEmoji)
|
||||
decryptedLabelEmoji = Emoji.unsafeCastMaybeInvalidStringToVariant(
|
||||
Bytes.toString(decryptGroupBlob(clientZkGroupCipher, member.labelEmoji))
|
||||
);
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
|
||||
@ -49,13 +49,14 @@ import {
|
||||
} from '../jobs/conversationJobQueue.preload.ts';
|
||||
import { maybeNotify } from '../messages/maybeNotify.preload.ts';
|
||||
import { itemStorage } from '../textsecure/Storage.preload.ts';
|
||||
import type { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { maxBy } = lodash;
|
||||
|
||||
const log = createLogger('Reactions');
|
||||
|
||||
export type ReactionAttributesType = {
|
||||
emoji: string;
|
||||
emoji: Emoji.Variant;
|
||||
envelopeId: string;
|
||||
fromId: string;
|
||||
remove?: boolean;
|
||||
|
||||
11
ts/model-types.d.ts
vendored
11
ts/model-types.d.ts
vendored
@ -45,6 +45,7 @@ import MemberRoleEnum = Proto.Member.Role;
|
||||
import type { MessageRequestResponseEvent } from './types/MessageRequestResponseEvent.std.ts';
|
||||
import type { QuotedMessageForComposerType } from './state/ducks/composer.preload.ts';
|
||||
import type { SEALED_SENDER } from './types/SealedSender.std.ts';
|
||||
import type { Emoji } from './axo/emoji.std.ts';
|
||||
|
||||
export type LastMessageStatus =
|
||||
| 'paused'
|
||||
@ -127,7 +128,7 @@ export type GroupV1Update = {
|
||||
};
|
||||
|
||||
export type MessageReactionType = {
|
||||
emoji: undefined | string;
|
||||
emoji: undefined | Emoji.Variant;
|
||||
fromId: string;
|
||||
targetTimestamp: number;
|
||||
timestamp: number;
|
||||
@ -252,7 +253,7 @@ export type MessageAttributesType = {
|
||||
contact?: ReadonlyArray<EmbeddedContactType>;
|
||||
conversationId: string;
|
||||
storyReaction?: {
|
||||
emoji: string;
|
||||
emoji: Emoji.Variant;
|
||||
targetAuthorAci: AciString;
|
||||
targetTimestamp: number;
|
||||
};
|
||||
@ -400,7 +401,7 @@ export type ConversationAttributesType = {
|
||||
lastMessageAuthorAci?: AciString | null;
|
||||
lastMessage?: string | null;
|
||||
lastMessageBodyRanges?: ReadonlyArray<RawBodyRange>;
|
||||
lastMessagePrefix?: string;
|
||||
lastMessagePrefix?: Emoji.Variant;
|
||||
/** @deprecated Use lastMessageAuthorAci instead */
|
||||
lastMessageAuthor?: string | null;
|
||||
lastMessageStatus?: LastMessageStatus | null;
|
||||
@ -464,7 +465,7 @@ export type ConversationAttributesType = {
|
||||
|
||||
// Private other fields
|
||||
about?: string;
|
||||
aboutEmoji?: string;
|
||||
aboutEmoji?: Emoji.Variant;
|
||||
profileFamilyName?: string;
|
||||
profileKey?: string;
|
||||
profileName?: string;
|
||||
@ -567,7 +568,7 @@ export type GroupV2MemberType = {
|
||||
role: MemberRoleEnum;
|
||||
joinedAtVersion: number;
|
||||
labelString?: string;
|
||||
labelEmoji?: string;
|
||||
labelEmoji?: Emoji.Variant;
|
||||
|
||||
// Note that these are temporary flags, generated by applyGroupChange, but eliminated
|
||||
// by applyGroupState. They are used to make our diff-generation more intelligent but
|
||||
|
||||
@ -270,6 +270,7 @@ import { missingCaseError } from '../util/missingCaseError.std.ts';
|
||||
import * as Message from '../types/Message2.preload.ts';
|
||||
import { itemStorage } from '../textsecure/Storage.preload.ts';
|
||||
import { isUsernameValid } from '../util/Username.dom.ts';
|
||||
import type { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { compact, isNumber, throttle, debounce } = lodash;
|
||||
|
||||
@ -4641,7 +4642,7 @@ export class ConversationModel {
|
||||
labelEmoji,
|
||||
labelString,
|
||||
}: {
|
||||
labelEmoji: string | undefined;
|
||||
labelEmoji: Emoji.Variant | undefined;
|
||||
labelString: string | undefined;
|
||||
}): Promise<void> {
|
||||
if (!isGroupV2(this.attributes)) {
|
||||
|
||||
@ -6,38 +6,33 @@ import Emitter from '@signalapp/quill-cjs/core/emitter.js';
|
||||
import type Quill from '@signalapp/quill-cjs';
|
||||
|
||||
import { createLogger } from '../../logging/log.std.ts';
|
||||
import type { EmojiParentKey } from '../../components/fun/data/emojis.std.ts';
|
||||
import {
|
||||
EMOJI_PARENT_KEY_CONSTANTS,
|
||||
EmojiSkinTone,
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
} from '../../components/fun/data/emojis.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const log = createLogger('index');
|
||||
|
||||
export type AutoSubstituteAsciiEmojisOptions = {
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
emojiSkinToneDefault: Emoji.SkinTone | null;
|
||||
};
|
||||
|
||||
type EmojiShortcutMap = Partial<Record<string, EmojiParentKey>>;
|
||||
type EmojiShortcutMap = Partial<Record<string, Emoji.Parent>>;
|
||||
|
||||
const emojiShortcutMap: EmojiShortcutMap = {
|
||||
':-)': EMOJI_PARENT_KEY_CONSTANTS.SLIGHTLY_SMILING_FACE,
|
||||
':-(': EMOJI_PARENT_KEY_CONSTANTS.SLIGHTLY_FROWNING_FACE,
|
||||
':-D': EMOJI_PARENT_KEY_CONSTANTS.GRINNING_FACE,
|
||||
':-*': EMOJI_PARENT_KEY_CONSTANTS.FACE_BLOWING_A_KISS,
|
||||
':-P': EMOJI_PARENT_KEY_CONSTANTS.FACE_WITH_STUCK_OUT_TONGUE,
|
||||
':-p': EMOJI_PARENT_KEY_CONSTANTS.FACE_WITH_STUCK_OUT_TONGUE,
|
||||
":'(": EMOJI_PARENT_KEY_CONSTANTS.CRYING_FACE,
|
||||
':-\\': EMOJI_PARENT_KEY_CONSTANTS.CONFUSED_FACE,
|
||||
':-|': EMOJI_PARENT_KEY_CONSTANTS.NEUTRAL_FACE,
|
||||
';-)': EMOJI_PARENT_KEY_CONSTANTS.WINKING_FACE,
|
||||
'(Y)': EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP,
|
||||
'(N)': EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP,
|
||||
'(y)': EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP,
|
||||
'(n)': EMOJI_PARENT_KEY_CONSTANTS.THUMBS_DOWN,
|
||||
'<3': EMOJI_PARENT_KEY_CONSTANTS.RED_HEART,
|
||||
'^_^': EMOJI_PARENT_KEY_CONSTANTS.GRINNING_FACE,
|
||||
':-)': Emoji.SLIGHTLY_SMILING_FACE,
|
||||
':-(': Emoji.SLIGHTLY_FROWNING_FACE,
|
||||
':-D': Emoji.GRINNING,
|
||||
':-*': Emoji.KISSING_HEART,
|
||||
':-P': Emoji.STUCK_OUT_TONGUE,
|
||||
':-p': Emoji.STUCK_OUT_TONGUE,
|
||||
":'(": Emoji.CRY,
|
||||
':-\\': Emoji.CONFUSED,
|
||||
':-|': Emoji.NEUTRAL_FACE,
|
||||
';-)': Emoji.WINK,
|
||||
'(Y)': Emoji.THUMBS_UP,
|
||||
'(N)': Emoji.THUMBS_UP,
|
||||
'(y)': Emoji.THUMBS_UP,
|
||||
'(n)': Emoji.THUMBS_DOWN,
|
||||
'<3': Emoji.HEART,
|
||||
'^_^': Emoji.GRINNING,
|
||||
};
|
||||
|
||||
function buildRegexp(obj: EmojiShortcutMap): RegExp {
|
||||
@ -110,11 +105,11 @@ export class AutoSubstituteAsciiEmojis {
|
||||
}
|
||||
|
||||
const [, textEmoji] = match as EmojiRegExpMatch;
|
||||
const emojiParentKey = emojiShortcutMap[textEmoji];
|
||||
const emoji = emojiShortcutMap[textEmoji];
|
||||
|
||||
if (emojiParentKey != null) {
|
||||
if (emoji != null) {
|
||||
this.insertEmoji(
|
||||
emojiParentKey,
|
||||
emoji,
|
||||
range.index - textEmoji.length,
|
||||
textEmoji.length,
|
||||
textEmoji
|
||||
@ -123,21 +118,18 @@ export class AutoSubstituteAsciiEmojis {
|
||||
}
|
||||
|
||||
insertEmoji(
|
||||
emojiParentKey: EmojiParentKey,
|
||||
parent: Emoji.Parent,
|
||||
index: number,
|
||||
range: number,
|
||||
source: string
|
||||
): void {
|
||||
const emojiVariant = getEmojiVariantByParentKeyAndSkinTone(
|
||||
emojiParentKey,
|
||||
this.options.emojiSkinToneDefault ?? EmojiSkinTone.None
|
||||
const value = Emoji.getVariant(
|
||||
parent,
|
||||
this.options.emojiSkinToneDefault ?? Emoji.SkinTone.None
|
||||
);
|
||||
const delta = new Delta()
|
||||
.retain(index)
|
||||
.delete(range)
|
||||
.insert({
|
||||
emoji: { value: emojiVariant.value, source },
|
||||
});
|
||||
const delta = new Delta().retain(index).delete(range).insert({
|
||||
emoji: { value, source },
|
||||
});
|
||||
this.quill.updateContents(delta, 'api');
|
||||
this.quill.setSelection(index + 1, 0);
|
||||
}
|
||||
|
||||
@ -2,22 +2,18 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import EmbedBlot from '@signalapp/quill-cjs/blots/embed.js';
|
||||
import type { EmojiVariantValue } from '../../components/fun/data/emojis.std.ts';
|
||||
import {
|
||||
getEmojiVariantByKey,
|
||||
getEmojiVariantKeyByValue,
|
||||
} from '../../components/fun/data/emojis.std.ts';
|
||||
import {
|
||||
createStaticEmojiBlot,
|
||||
FUN_STATIC_EMOJI_CLASS,
|
||||
getFunEmojiElementValue,
|
||||
} from '../../components/fun/FunEmoji.dom.tsx';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
// the DOM structure of this EmojiBlot should match the other emoji implementations:
|
||||
// ts/components/fun/FunEmoji.tsx
|
||||
|
||||
export type EmojiBlotValue = Readonly<{
|
||||
value: EmojiVariantValue;
|
||||
value: Emoji.Variant;
|
||||
source?: string;
|
||||
}>;
|
||||
|
||||
@ -29,20 +25,16 @@ export class EmojiBlot extends EmbedBlot {
|
||||
|
||||
static override className = FUN_STATIC_EMOJI_CLASS;
|
||||
|
||||
static override create({ value: emoji, source }: EmojiBlotValue): Node {
|
||||
static override create({ value, source }: EmojiBlotValue): Node {
|
||||
const node = super.create(undefined) as HTMLSpanElement;
|
||||
|
||||
const variantKey = getEmojiVariantKeyByValue(emoji);
|
||||
const variant = getEmojiVariantByKey(variantKey);
|
||||
|
||||
createStaticEmojiBlot(node, {
|
||||
role: 'img',
|
||||
'aria-label': emoji,
|
||||
emoji: variant,
|
||||
'aria-label': Emoji.getDisplayLabel(value),
|
||||
emoji: value,
|
||||
size: 20,
|
||||
});
|
||||
node.setAttribute('data-emoji-key', variantKey);
|
||||
node.setAttribute('data-emoji-value', emoji);
|
||||
node.setAttribute('data-emoji', value);
|
||||
node.setAttribute('data-source', source ?? '');
|
||||
|
||||
return node;
|
||||
|
||||
@ -15,19 +15,9 @@ import { getBlotTextPartitions, matchBlotTextPartitions } from '../util.dom.ts';
|
||||
import { handleOutsideClick } from '../../util/handleOutsideClick.dom.ts';
|
||||
import { createLogger } from '../../logging/log.std.ts';
|
||||
import { FunStaticEmoji } from '../../components/fun/FunEmoji.dom.tsx';
|
||||
import type { EmojiParentKey } from '../../components/fun/data/emojis.std.ts';
|
||||
import {
|
||||
EmojiSkinTone,
|
||||
getEmojiVariantByParentKeyAndSkinTone,
|
||||
normalizeShortNameCompletionDisplay,
|
||||
} from '../../components/fun/data/emojis.std.ts';
|
||||
import type {
|
||||
FunEmojiSearchResult,
|
||||
FunEmojiSearch,
|
||||
} from '../../components/fun/useFunEmojiSearch.dom.tsx';
|
||||
import { type FunEmojiLocalizer } from '../../components/fun/useFunEmojiLocalizer.dom.tsx';
|
||||
import type { FunEmojiSelection } from '../../components/fun/panels/FunPanelEmojis.dom.tsx';
|
||||
import { strictAssert } from '../../util/assert.std.ts';
|
||||
import { Emoji } from '../../axo/emoji.std.ts';
|
||||
|
||||
const { isNumber, debounce } = lodash;
|
||||
|
||||
@ -36,13 +26,11 @@ const log = createLogger('completion');
|
||||
export type EmojiCompletionOptions = {
|
||||
onSelectEmoji: (emojiSelection: FunEmojiSelection) => void;
|
||||
setEmojiPickerElement: (element: JSX.Element | null) => void;
|
||||
emojiSkinToneDefault: EmojiSkinTone | null;
|
||||
emojiSearch: FunEmojiSearch;
|
||||
emojiLocalizer: FunEmojiLocalizer;
|
||||
emojiSkinToneDefault: Emoji.SkinTone | null;
|
||||
};
|
||||
|
||||
export type InsertEmojiOptionsType = Readonly<{
|
||||
emojiParentKey: EmojiParentKey;
|
||||
emoji: Emoji.Parent;
|
||||
index: number;
|
||||
range: number;
|
||||
withTrailingSpace?: boolean;
|
||||
@ -50,7 +38,7 @@ export type InsertEmojiOptionsType = Readonly<{
|
||||
}>;
|
||||
|
||||
export class EmojiCompletion {
|
||||
results: ReadonlyArray<FunEmojiSearchResult>;
|
||||
results: ReadonlyArray<Emoji.Parent>;
|
||||
|
||||
index: number;
|
||||
|
||||
@ -174,13 +162,12 @@ export class EmojiCompletion {
|
||||
leftTokenTextMatch as LeftTokenMatch;
|
||||
|
||||
if (isSelfClosing || justPressedColon) {
|
||||
const parentKey =
|
||||
this.options.emojiLocalizer.getParentKeyForText(leftTokenText);
|
||||
if (parentKey != null) {
|
||||
const parent = Emoji.matchShortName(leftTokenText);
|
||||
if (parent != null) {
|
||||
const numberOfColons = isSelfClosing ? 2 : 1;
|
||||
|
||||
this.insertEmoji({
|
||||
emojiParentKey: parentKey,
|
||||
emoji: parent,
|
||||
index: range.index - leftTokenText.length - numberOfColons,
|
||||
range: leftTokenText.length + numberOfColons,
|
||||
justPressedColon,
|
||||
@ -194,12 +181,11 @@ export class EmojiCompletion {
|
||||
if (rightTokenTextMatch) {
|
||||
const [, rightTokenText] = rightTokenTextMatch as RightTokenMatch;
|
||||
const tokenText = leftTokenText + rightTokenText;
|
||||
const parentKey =
|
||||
this.options.emojiLocalizer.getParentKeyForText(tokenText);
|
||||
const parent = Emoji.matchShortName(tokenText);
|
||||
|
||||
if (parentKey != null) {
|
||||
if (parent != null) {
|
||||
this.insertEmoji({
|
||||
emojiParentKey: parentKey,
|
||||
emoji: parent,
|
||||
index: range.index - leftTokenText.length - 1,
|
||||
range: tokenText.length + 2,
|
||||
justPressedColon,
|
||||
@ -213,7 +199,7 @@ export class EmojiCompletion {
|
||||
return PASS_THROUGH;
|
||||
}
|
||||
|
||||
const showEmojiResults = this.options.emojiSearch(leftTokenText, 10);
|
||||
const showEmojiResults = Emoji.search(leftTokenText, 10);
|
||||
|
||||
if (showEmojiResults.length > 0) {
|
||||
this.results = showEmojiResults;
|
||||
@ -262,7 +248,7 @@ export class EmojiCompletion {
|
||||
const [, tokenText] = tokenTextMatch as TokenTextMatch;
|
||||
|
||||
this.insertEmoji({
|
||||
emojiParentKey: result.parentKey,
|
||||
emoji: result,
|
||||
index: range.index - tokenText.length - 1,
|
||||
range: tokenText.length + 1,
|
||||
withTrailingSpace: true,
|
||||
@ -270,17 +256,14 @@ export class EmojiCompletion {
|
||||
}
|
||||
|
||||
insertEmoji({
|
||||
emojiParentKey,
|
||||
emoji,
|
||||
index,
|
||||
range,
|
||||
withTrailingSpace = false,
|
||||
justPressedColon = false,
|
||||
}: InsertEmojiOptionsType): void {
|
||||
const skinTone = this.options.emojiSkinToneDefault ?? EmojiSkinTone.None;
|
||||
const emojiVariant = getEmojiVariantByParentKeyAndSkinTone(
|
||||
emojiParentKey,
|
||||
skinTone
|
||||
);
|
||||
const skinTone = this.options.emojiSkinToneDefault ?? Emoji.SkinTone.None;
|
||||
const variant = Emoji.getVariant(emoji, skinTone);
|
||||
|
||||
let source = this.quill.getText(index, range);
|
||||
if (justPressedColon) {
|
||||
@ -291,7 +274,7 @@ export class EmojiCompletion {
|
||||
.retain(index)
|
||||
.delete(range)
|
||||
.insert({
|
||||
emoji: { value: emojiVariant.value, source },
|
||||
emoji: { value: variant, source },
|
||||
});
|
||||
|
||||
if (withTrailingSpace) {
|
||||
@ -305,7 +288,7 @@ export class EmojiCompletion {
|
||||
}
|
||||
|
||||
this.options.onSelectEmoji({
|
||||
variantKey: emojiVariant.key,
|
||||
emoji: variant,
|
||||
});
|
||||
|
||||
this.reset();
|
||||
@ -385,31 +368,23 @@ export class EmojiCompletion {
|
||||
role="listbox"
|
||||
aria-expanded
|
||||
aria-activedescendant={`emoji-result--${
|
||||
emojiResults.length
|
||||
? emojiResults[emojiResultsIndex]?.parentKey
|
||||
: ''
|
||||
emojiResults.length ? emojiResults[emojiResultsIndex] : ''
|
||||
}`}
|
||||
tabIndex={0}
|
||||
>
|
||||
{emojiResults.map((result, index) => {
|
||||
const emojiVariant = getEmojiVariantByParentKeyAndSkinTone(
|
||||
result.parentKey,
|
||||
this.options.emojiSkinToneDefault ?? EmojiSkinTone.None
|
||||
const emojiVariant = Emoji.getVariant(
|
||||
result,
|
||||
this.options.emojiSkinToneDefault ?? Emoji.SkinTone.None
|
||||
);
|
||||
|
||||
const localeShortName =
|
||||
this.options.emojiLocalizer.getLocaleShortName(
|
||||
emojiVariant.key
|
||||
);
|
||||
|
||||
const normalized =
|
||||
normalizeShortNameCompletionDisplay(localeShortName);
|
||||
const completionLabel = Emoji.getCompletionLabel(result);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={result.parentKey}
|
||||
id={`emoji-result--${result.parentKey}`}
|
||||
key={result}
|
||||
id={`emoji-result--${result}`}
|
||||
role="option button"
|
||||
aria-selected={emojiResultsIndex === index}
|
||||
onClick={() => {
|
||||
@ -429,7 +404,7 @@ export class EmojiCompletion {
|
||||
size={16}
|
||||
/>
|
||||
<div className="module-composition-input__suggestions__row__short-name">
|
||||
:{normalized}:
|
||||
:{completionLabel}:
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user