Fix localized emoji auto-completions
This commit is contained in:
parent
dd3ab66593
commit
074fa8af4b
11 changed files with 66 additions and 49 deletions
|
|
@ -1271,7 +1271,7 @@ function useReactionsToast(props: UseReactionsToastType): void {
|
||||||
<span className="CallingReactionsToasts__reaction">
|
<span className="CallingReactionsToasts__reaction">
|
||||||
<FunStaticEmoji
|
<FunStaticEmoji
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={emojiLocalizer(emojiVariantKey)}
|
aria-label={emojiLocalizer.getLocaleShortName(emojiVariantKey)}
|
||||||
size={28}
|
size={28}
|
||||||
emoji={emojiVariant}
|
emoji={emojiVariant}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@ function BioEmoji(props: { emoji: EmojiVariantKey }) {
|
||||||
return (
|
return (
|
||||||
<FunStaticEmoji
|
<FunStaticEmoji
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={emojiLocalizer(props.emoji)}
|
aria-label={emojiLocalizer.getLocaleShortName(props.emoji)}
|
||||||
emoji={emojiVariant}
|
emoji={emojiVariant}
|
||||||
size={24}
|
size={24}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export function Emojify({
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
key={index}
|
key={index}
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={emojiLocalizer(variantKey)}
|
aria-label={emojiLocalizer.getLocaleShortName(variantKey)}
|
||||||
emoji={variant}
|
emoji={variant}
|
||||||
size={fontSizeOverride}
|
size={fontSizeOverride}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ function ReactionViewerEmoji(props: {
|
||||||
return (
|
return (
|
||||||
<FunStaticEmoji
|
<FunStaticEmoji
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={emojiLocalizer(emojiVariantKey)}
|
aria-label={emojiLocalizer.getLocaleShortName(emojiVariantKey)}
|
||||||
size={18}
|
size={18}
|
||||||
emoji={emojiVariant}
|
emoji={emojiVariant}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,7 @@
|
||||||
|
|
||||||
// Camelcase disabled due to emoji-datasource using snake_case
|
// Camelcase disabled due to emoji-datasource using snake_case
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
import {
|
import { groupBy, keyBy, mapValues, sortBy } from 'lodash';
|
||||||
compact,
|
|
||||||
flatMap,
|
|
||||||
groupBy,
|
|
||||||
keyBy,
|
|
||||||
map,
|
|
||||||
mapValues,
|
|
||||||
sortBy,
|
|
||||||
} from 'lodash';
|
|
||||||
import { getOwn } from '../../util/getOwn';
|
import { getOwn } from '../../util/getOwn';
|
||||||
import {
|
import {
|
||||||
EMOJI_SKIN_TONE_TO_KEY,
|
EMOJI_SKIN_TONE_TO_KEY,
|
||||||
|
|
@ -152,15 +144,6 @@ export function getEmojiData(
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shortNames = new Set([
|
|
||||||
...map(data, 'short_name'),
|
|
||||||
...compact<string>(flatMap(data, 'short_names')),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function isShortName(name: string): boolean {
|
|
||||||
return shortNames.has(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unifiedToEmoji(unified: string): string {
|
export function unifiedToEmoji(unified: string): string {
|
||||||
return unified
|
return unified
|
||||||
.split('-')
|
.split('-')
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export function FunEmojiPickerButton(
|
||||||
<FunStaticEmoji
|
<FunStaticEmoji
|
||||||
role="img"
|
role="img"
|
||||||
size={20}
|
size={20}
|
||||||
aria-label={emojiLocalizer(emojiVarant.key)}
|
aria-label={emojiLocalizer.getLocaleShortName(emojiVarant.key)}
|
||||||
emoji={emojiVarant}
|
emoji={emojiVarant}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,10 @@ type EmojiIndex = Readonly<{
|
||||||
pickerCategories: Record<EmojiPickerCategory, Array<EmojiParentKey>>;
|
pickerCategories: Record<EmojiPickerCategory, Array<EmojiParentKey>>;
|
||||||
|
|
||||||
defaultEnglishSearchIndex: Array<FunEmojiSearchIndexEntry>;
|
defaultEnglishSearchIndex: Array<FunEmojiSearchIndexEntry>;
|
||||||
defaultEnglishLocalizerIndex: Map<EmojiParentKey, string>;
|
defaultEnglishLocalizerIndex: {
|
||||||
|
parentKeyToLocaleShortName: Map<EmojiParentKey, string>;
|
||||||
|
localeShortNameToParentKey: Map<string, EmojiParentKey>;
|
||||||
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
|
@ -288,7 +291,10 @@ const EMOJI_INDEX: EmojiIndex = {
|
||||||
[EmojiPickerCategory.Flags]: [],
|
[EmojiPickerCategory.Flags]: [],
|
||||||
},
|
},
|
||||||
defaultEnglishSearchIndex: [],
|
defaultEnglishSearchIndex: [],
|
||||||
defaultEnglishLocalizerIndex: new Map(),
|
defaultEnglishLocalizerIndex: {
|
||||||
|
parentKeyToLocaleShortName: new Map(),
|
||||||
|
localeShortNameToParentKey: new Map(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function addParent(parent: EmojiParentData, rank: number) {
|
function addParent(parent: EmojiParentData, rank: number) {
|
||||||
|
|
@ -320,10 +326,14 @@ function addParent(parent: EmojiParentData, rank: number) {
|
||||||
emoticons: parent.emoticons,
|
emoticons: parent.emoticons,
|
||||||
});
|
});
|
||||||
|
|
||||||
EMOJI_INDEX.defaultEnglishLocalizerIndex.set(
|
EMOJI_INDEX.defaultEnglishLocalizerIndex.parentKeyToLocaleShortName.set(
|
||||||
parent.key,
|
parent.key,
|
||||||
parent.englishShortNameDefault
|
parent.englishShortNameDefault
|
||||||
);
|
);
|
||||||
|
EMOJI_INDEX.defaultEnglishLocalizerIndex.localeShortNameToParentKey.set(
|
||||||
|
parent.englishShortNameDefault,
|
||||||
|
parent.key
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addVariant(parentKey: EmojiParentKey, variant: EmojiVariantData) {
|
function addVariant(parentKey: EmojiParentKey, variant: EmojiVariantData) {
|
||||||
|
|
|
||||||
|
|
@ -641,7 +641,7 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
|
||||||
);
|
);
|
||||||
|
|
||||||
const emojiName = useMemo(() => {
|
const emojiName = useMemo(() => {
|
||||||
return emojiLocalizer(emojiVariant.key);
|
return emojiLocalizer.getLocaleShortName(emojiVariant.key);
|
||||||
}, [emojiVariant.key, emojiLocalizer]);
|
}, [emojiVariant.key, emojiLocalizer]);
|
||||||
|
|
||||||
const emojiShortNameDisplay = useMemo(() => {
|
const emojiShortNameDisplay = useMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,21 @@ import type { LocaleEmojiListType } from '../../types/emoji';
|
||||||
import { strictAssert } from '../../util/assert';
|
import { strictAssert } from '../../util/assert';
|
||||||
import { useFunEmojiLocalization } from './FunEmojiLocalizationProvider';
|
import { useFunEmojiLocalization } from './FunEmojiLocalizationProvider';
|
||||||
|
|
||||||
export type FunEmojiLocalizerIndex = ReadonlyMap<EmojiParentKey, string>;
|
export type FunEmojiLocalizerIndex = Readonly<{
|
||||||
export type FunEmojiLocalizer = (key: EmojiVariantKey) => string;
|
parentKeyToLocaleShortName: ReadonlyMap<EmojiParentKey, string>;
|
||||||
|
localeShortNameToParentKey: ReadonlyMap<string, EmojiParentKey>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type FunEmojiLocalizer = Readonly<{
|
||||||
|
getLocaleShortName: (key: EmojiVariantKey) => string;
|
||||||
|
getParentKeyForText: (text: string) => EmojiParentKey | null;
|
||||||
|
}>;
|
||||||
|
|
||||||
export function createFunEmojiLocalizerIndex(
|
export function createFunEmojiLocalizerIndex(
|
||||||
localeEmojiList: LocaleEmojiListType
|
localeEmojiList: LocaleEmojiListType
|
||||||
): FunEmojiLocalizerIndex {
|
): FunEmojiLocalizerIndex {
|
||||||
const index = new Map<EmojiParentKey, string>();
|
const parentKeyToLocaleShortName = new Map<EmojiParentKey, string>();
|
||||||
|
const localeShortNameToParentKey = new Map<string, EmojiParentKey>();
|
||||||
|
|
||||||
for (const entry of localeEmojiList) {
|
for (const entry of localeEmojiList) {
|
||||||
strictAssert(
|
strictAssert(
|
||||||
|
|
@ -29,26 +37,35 @@ export function createFunEmojiLocalizerIndex(
|
||||||
const variantKey = getEmojiVariantKeyByValue(entry.emoji);
|
const variantKey = getEmojiVariantKeyByValue(entry.emoji);
|
||||||
const parentKey = getEmojiParentKeyByVariantKey(variantKey);
|
const parentKey = getEmojiParentKeyByVariantKey(variantKey);
|
||||||
const localizedShortName = entry.tags.at(0) ?? entry.shortName;
|
const localizedShortName = entry.tags.at(0) ?? entry.shortName;
|
||||||
index.set(parentKey, localizedShortName);
|
parentKeyToLocaleShortName.set(parentKey, localizedShortName);
|
||||||
|
localeShortNameToParentKey.set(localizedShortName, parentKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
return index;
|
return { parentKeyToLocaleShortName, localeShortNameToParentKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal exported for tests */
|
/** @internal exported for tests */
|
||||||
export function _createFunEmojiLocalizer(
|
export function _createFunEmojiLocalizer(
|
||||||
emojiLocalizerIndex: FunEmojiLocalizerIndex
|
emojiLocalizerIndex: FunEmojiLocalizerIndex
|
||||||
): FunEmojiLocalizer {
|
): FunEmojiLocalizer {
|
||||||
return variantKey => {
|
function getLocaleShortName(variantKey: EmojiVariantKey) {
|
||||||
const parentKey = getEmojiParentKeyByVariantKey(variantKey);
|
const parentKey = getEmojiParentKeyByVariantKey(variantKey);
|
||||||
const localeShortName = emojiLocalizerIndex.get(parentKey);
|
const localeShortName =
|
||||||
|
emojiLocalizerIndex.parentKeyToLocaleShortName.get(parentKey);
|
||||||
if (localeShortName != null) {
|
if (localeShortName != null) {
|
||||||
return localeShortName;
|
return localeShortName;
|
||||||
}
|
}
|
||||||
// Fallback to english short name
|
// Fallback to english short name
|
||||||
const parent = getEmojiParentByKey(parentKey);
|
const parent = getEmojiParentByKey(parentKey);
|
||||||
return parent.englishShortNameDefault;
|
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 {
|
export function useFunEmojiLocalizer(): FunEmojiLocalizer {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ import Emitter from '@signalapp/quill-cjs/core/emitter';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import _, { isNumber } from 'lodash';
|
import _, { isNumber } from 'lodash';
|
||||||
import type Quill from '@signalapp/quill-cjs';
|
import type Quill from '@signalapp/quill-cjs';
|
||||||
|
|
||||||
import { Popper } from 'react-popper';
|
import { Popper } from 'react-popper';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import type { VirtualElement } from '@popperjs/core';
|
import type { VirtualElement } from '@popperjs/core';
|
||||||
import { convertShortName, isShortName } from '../../components/emoji/lib';
|
import { convertShortName } from '../../components/emoji/lib';
|
||||||
import type { EmojiPickDataType } from '../../components/emoji/EmojiPicker';
|
import type { EmojiPickDataType } from '../../components/emoji/EmojiPicker';
|
||||||
import { getBlotTextPartitions, matchBlotTextPartitions } from '../util';
|
import { getBlotTextPartitions, matchBlotTextPartitions } from '../util';
|
||||||
import { handleOutsideClick } from '../../util/handleOutsideClick';
|
import { handleOutsideClick } from '../../util/handleOutsideClick';
|
||||||
|
|
@ -23,8 +22,10 @@ import {
|
||||||
getEmojiVariantByParentKeyAndSkinTone,
|
getEmojiVariantByParentKeyAndSkinTone,
|
||||||
normalizeShortNameCompletionDisplay,
|
normalizeShortNameCompletionDisplay,
|
||||||
} from '../../components/fun/data/emojis';
|
} from '../../components/fun/data/emojis';
|
||||||
import type { FunEmojiSearchResult } from '../../components/fun/useFunEmojiSearch';
|
import type {
|
||||||
import { type FunEmojiSearch } from '../../components/fun/useFunEmojiSearch';
|
FunEmojiSearchResult,
|
||||||
|
FunEmojiSearch,
|
||||||
|
} from '../../components/fun/useFunEmojiSearch';
|
||||||
import { type FunEmojiLocalizer } from '../../components/fun/useFunEmojiLocalizer';
|
import { type FunEmojiLocalizer } from '../../components/fun/useFunEmojiLocalizer';
|
||||||
|
|
||||||
export type EmojiCompletionOptions = {
|
export type EmojiCompletionOptions = {
|
||||||
|
|
@ -164,11 +165,14 @@ export class EmojiCompletion {
|
||||||
const [, leftTokenText, isSelfClosing] = leftTokenTextMatch;
|
const [, leftTokenText, isSelfClosing] = leftTokenTextMatch;
|
||||||
|
|
||||||
if (isSelfClosing || justPressedColon) {
|
if (isSelfClosing || justPressedColon) {
|
||||||
if (isShortName(leftTokenText)) {
|
const parentKey =
|
||||||
|
this.options.emojiLocalizer.getParentKeyForText(leftTokenText);
|
||||||
|
if (parentKey != null) {
|
||||||
const numberOfColons = isSelfClosing ? 2 : 1;
|
const numberOfColons = isSelfClosing ? 2 : 1;
|
||||||
|
const emoji = getEmojiParentByKey(parentKey);
|
||||||
|
|
||||||
this.insertEmoji({
|
this.insertEmoji({
|
||||||
shortName: leftTokenText,
|
shortName: emoji.englishShortNameDefault,
|
||||||
index: range.index - leftTokenText.length - numberOfColons,
|
index: range.index - leftTokenText.length - numberOfColons,
|
||||||
range: leftTokenText.length + numberOfColons,
|
range: leftTokenText.length + numberOfColons,
|
||||||
justPressedColon,
|
justPressedColon,
|
||||||
|
|
@ -182,10 +186,13 @@ export class EmojiCompletion {
|
||||||
if (rightTokenTextMatch) {
|
if (rightTokenTextMatch) {
|
||||||
const [, rightTokenText] = rightTokenTextMatch;
|
const [, rightTokenText] = rightTokenTextMatch;
|
||||||
const tokenText = leftTokenText + rightTokenText;
|
const tokenText = leftTokenText + rightTokenText;
|
||||||
|
const parentKey =
|
||||||
|
this.options.emojiLocalizer.getParentKeyForText(tokenText);
|
||||||
|
|
||||||
if (isShortName(tokenText)) {
|
if (parentKey != null) {
|
||||||
|
const emoji = getEmojiParentByKey(parentKey);
|
||||||
this.insertEmoji({
|
this.insertEmoji({
|
||||||
shortName: tokenText,
|
shortName: emoji.englishShortNameDefault,
|
||||||
index: range.index - leftTokenText.length - 1,
|
index: range.index - leftTokenText.length - 1,
|
||||||
range: tokenText.length + 2,
|
range: tokenText.length + 2,
|
||||||
justPressedColon,
|
justPressedColon,
|
||||||
|
|
@ -377,9 +384,10 @@ export class EmojiCompletion {
|
||||||
this.options.emojiSkinToneDefault ?? EmojiSkinTone.None
|
this.options.emojiSkinToneDefault ?? EmojiSkinTone.None
|
||||||
);
|
);
|
||||||
|
|
||||||
const localeShortName = this.options.emojiLocalizer(
|
const localeShortName =
|
||||||
emojiVariant.key
|
this.options.emojiLocalizer.getLocaleShortName(
|
||||||
);
|
emojiVariant.key
|
||||||
|
);
|
||||||
|
|
||||||
const normalized =
|
const normalized =
|
||||||
normalizeShortNameCompletionDisplay(localeShortName);
|
normalizeShortNameCompletionDisplay(localeShortName);
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,12 @@
|
||||||
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
import { isShortName } from '../../components/emoji/lib';
|
import { isEmojiEnglishShortName } from '../../components/fun/data/emojis';
|
||||||
|
|
||||||
export const selectRecentEmojis = createSelector(
|
export const selectRecentEmojis = createSelector(
|
||||||
({ emojis }: StateType) => emojis.recents,
|
({ emojis }: StateType) => emojis.recents,
|
||||||
recents => recents.filter(isShortName)
|
recents => recents.filter(isEmojiEnglishShortName)
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useRecentEmojis = (): Array<string> =>
|
export const useRecentEmojis = (): Array<string> =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue