Translate emoji completions in composer

This commit is contained in:
Jamie Kyle 2025-04-23 16:03:35 -07:00 committed by GitHub
commit e802ea0dc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 216 additions and 273 deletions

View file

@ -60,7 +60,6 @@ import { getClassNamesFor } from '../util/getClassNamesFor';
import { isNotNil } from '../util/isNotNil';
import * as log from '../logging/log';
import * as Errors from '../types/errors';
import { useEmojiSearch } from '../hooks/useEmojiSearch';
import type { LinkPreviewForUIType } from '../types/message/LinkPreviews';
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
import type { DraftEditMessageType } from '../model-types.d';
@ -79,6 +78,9 @@ import { dropNull } from '../util/dropNull';
import { SimpleQuillWrapper } from './SimpleQuillWrapper';
import type { EmojiSkinTone } from './fun/data/emojis';
import { FUN_STATIC_EMOJI_CLASS } from './fun/FunEmoji';
import { useFunEmojiSearch } from './fun/useFunEmojiSearch';
import type { EmojiCompletionOptions } from '../quill/emoji/completion';
import { useFunEmojiLocalizer } from './fun/useFunEmojiLocalizer';
Quill.register(
{
@ -192,7 +194,7 @@ export function CompositionInput(props: Props): React.ReactElement {
} = props;
const [emojiCompletionElement, setEmojiCompletionElement] =
React.useState<JSX.Element>();
React.useState<JSX.Element | null>();
const [formattingChooserElement, setFormattingChooserElement] =
React.useState<JSX.Element>();
const [lastSelectionRange, setLastSelectionRange] =
@ -770,7 +772,8 @@ export function CompositionInput(props: Props): React.ReactElement {
const callbacksRef = React.useRef(unstaleCallbacks);
callbacksRef.current = unstaleCallbacks;
const search = useEmojiSearch(i18n.getLocale());
const emojiSearch = useFunEmojiSearch();
const emojiLocalizer = useFunEmojiLocalizer();
const reactQuill = React.useMemo(
() => {
@ -839,8 +842,9 @@ export function CompositionInput(props: Props): React.ReactElement {
onPickEmoji: (emoji: EmojiPickDataType) =>
callbacksRef.current.onPickEmoji(emoji),
emojiSkinToneDefault,
search,
},
emojiSearch,
emojiLocalizer,
} satisfies EmojiCompletionOptions,
autoSubstituteAsciiEmojis: {
emojiSkinToneDefault,
} satisfies AutoSubstituteAsciiEmojisOptions,

View file

@ -23,7 +23,6 @@ import { dataByCategory } from './lib';
import type { LocalizerType } from '../../types/Util';
import { isSingleGrapheme } from '../../util/grapheme';
import { missingCaseError } from '../../util/missingCaseError';
import { useEmojiSearch } from '../../hooks/useEmojiSearch';
import { FunStaticEmoji } from '../fun/FunEmoji';
import { strictAssert } from '../../util/assert';
import {
@ -35,7 +34,9 @@ import {
getEmojiVariantByParentKeyAndSkinTone,
isEmojiEnglishShortName,
EMOJI_SKIN_TONE_TO_NUMBER,
getEmojiParentByKey,
} from '../fun/data/emojis';
import { useFunEmojiSearch } from '../fun/useFunEmojiSearch';
export type EmojiPickDataType = {
skinTone: EmojiSkinTone;
@ -122,7 +123,7 @@ export const EmojiPicker = React.memo(
const [selectedTone, setSelectedTone] =
React.useState(emojiSkinToneDefault);
const search = useEmojiSearch(i18n.getLocale());
const emojiSearch = useFunEmojiSearch();
const handleToggleSearch = React.useCallback(
(
@ -261,7 +262,13 @@ export const EmojiPicker = React.memo(
const emojiGrid = React.useMemo(() => {
if (searchText) {
return chunk(search(searchText), COL_COUNT);
return chunk(
emojiSearch(searchText).map(result => {
const parent = getEmojiParentByKey(result.parentKey);
return parent.englishShortNameDefault;
}),
COL_COUNT
);
}
const chunks = flatMap(renderableCategories, cat =>
@ -272,7 +279,7 @@ export const EmojiPicker = React.memo(
);
return [...chunk(firstRecent, COL_COUNT), ...chunks];
}, [firstRecent, renderableCategories, searchText, search]);
}, [firstRecent, renderableCategories, searchText, emojiSearch]);
const rowCount = emojiGrid.length;

View file

@ -3,7 +3,6 @@
// Camelcase disabled due to emoji-datasource using snake_case
/* eslint-disable camelcase */
import Fuse from 'fuse.js';
import {
compact,
flatMap,
@ -12,9 +11,7 @@ import {
map,
mapValues,
sortBy,
take,
} from 'lodash';
import type { LocaleEmojiType } from '../../types/emoji';
import { getOwn } from '../../util/getOwn';
import {
EMOJI_SKIN_TONE_TO_KEY,
@ -155,103 +152,6 @@ export function getEmojiData(
return base;
}
export type SearchFnType = (query: string, count?: number) => Array<string>;
export type SearchEmojiListType = ReadonlyArray<
Pick<LocaleEmojiType, 'shortName' | 'rank' | 'tags'>
>;
type CachedSearchFnType = Readonly<{
localeEmoji: SearchEmojiListType;
fn: SearchFnType;
}>;
let cachedSearchFn: CachedSearchFnType | undefined;
export function createSearch(localeEmoji: SearchEmojiListType): SearchFnType {
if (cachedSearchFn && cachedSearchFn.localeEmoji === localeEmoji) {
return cachedSearchFn.fn;
}
const knownSet = new Set<string>();
const knownEmoji = localeEmoji.filter(({ shortName }) => {
knownSet.add(shortName);
return dataByShortName[shortName] != null;
});
for (const entry of data) {
if (!knownSet.has(entry.short_name)) {
knownEmoji.push({
shortName: entry.short_name,
rank: 0,
tags: entry.short_names,
});
}
}
let maxShortNameLength = 0;
for (const { shortName } of knownEmoji) {
maxShortNameLength = Math.max(maxShortNameLength, shortName.length);
}
const fuse = new Fuse(knownEmoji, {
shouldSort: false,
threshold: 0.2,
minMatchCharLength: 1,
keys: ['shortName', 'tags'],
includeScore: true,
});
const fuseExactPrefix = new Fuse(knownEmoji, {
// We re-rank and sort manually below
shouldSort: false,
threshold: 0, // effectively a prefix search
minMatchCharLength: 2,
keys: ['shortName', 'tags'],
includeScore: true,
});
const fn = (query: string, count = 0): Array<string> => {
// when we only have 2 characters, do an exact prefix match
// to avoid matching on emoticon, like :-P
const fuseIndex = query.length === 2 ? fuseExactPrefix : fuse;
const rawResults = fuseIndex.search(query.substr(0, 32));
const rankedResults = rawResults.map(entry => {
const rank = entry.item.rank || 1e9;
// Rank exact prefix matches in [0,1] range
if (entry.item.shortName.startsWith(query)) {
return {
score: entry.item.shortName.length / maxShortNameLength,
item: entry.item,
};
}
// Other matches in [1,], ordered by score and rank
return {
score: 1 + (entry.score ?? 0) + rank / knownEmoji.length,
item: entry.item,
};
});
const results = rankedResults
.sort((a, b) => a.score - b.score)
.map(result => result.item.shortName);
if (count) {
return take(results, count);
}
return results;
};
cachedSearchFn = { localeEmoji, fn };
return fn;
}
const shortNames = new Set([
...map(data, 'short_name'),
...compact<string>(flatMap(data, 'short_names')),

View file

@ -9,6 +9,7 @@ import type {
FunEmojiSearchIndexEntry,
} from '../useFunEmojiSearch';
import type { FunEmojiLocalizerIndex } from '../useFunEmojiLocalizer';
import { removeDiacritics } from '../../../util/removeDiacritics';
// Import emoji-datasource dynamically to avoid costly typechecking.
// eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-var-requires
@ -551,6 +552,26 @@ export function emojiVariantConstant(input: string): EmojiVariantData {
return getEmojiVariantByKey(key);
}
/**
* Completions
*/
/** For displaying in the ui */
export function normalizeShortNameCompletionDisplay(shortName: string): string {
return removeDiacritics(shortName)
.normalize('NFD')
.replaceAll(' ', '_')
.toLowerCase();
}
/** For matching in search utils */
export function normalizeShortNameCompletionQuery(query: string): string {
return removeDiacritics(query)
.normalize('NFD')
.replaceAll(/[\s_-]+/gi, ' ')
.toLowerCase();
}
/**
* Emojify
*/

View file

@ -186,7 +186,9 @@ export function FunPanelEmojis({
return [
toGridSectionNode(
FunSectionCommon.SearchResults,
searchEmojis(searchQuery)
searchEmojis(searchQuery).map(result => {
return result.parentKey;
})
),
];
}

View file

@ -56,6 +56,7 @@ import {
FunSubNavScroller,
} from '../base/FunSubNav';
import {
type EmojiParentKey,
emojiVariantConstant,
getEmojiParentKeyByValue,
isEmojiParentValue,
@ -228,7 +229,11 @@ export function FunPanelStickers({
const sections = useMemo(() => {
if (searchQuery !== '') {
const emojiKeys = new Set(searchEmojis(searchQuery));
const emojiKeys = new Set<EmojiParentKey>();
for (const result of searchEmojis(searchQuery)) {
emojiKeys.add(result.parentKey);
}
const allStickers = installedStickerPacks.flatMap(pack => pack.stickers);
const matchingStickers = allStickers.filter(sticker => {

View file

@ -1,6 +1,6 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useCallback } from 'react';
import { useMemo } from 'react';
import type { EmojiParentKey, EmojiVariantKey } from './data/emojis';
import {
getEmojiParentByKey,
@ -28,26 +28,33 @@ export function createFunEmojiLocalizerIndex(
const variantKey = getEmojiVariantKeyByValue(entry.emoji);
const parentKey = getEmojiParentKeyByVariantKey(variantKey);
index.set(parentKey, entry.tags.at(0) ?? entry.shortName);
const localizedShortName = entry.tags.at(0) ?? entry.shortName;
index.set(parentKey, localizedShortName);
}
return index;
}
/** @internal exported for tests */
export function _createFunEmojiLocalizer(
emojiLocalizerIndex: FunEmojiLocalizerIndex
): FunEmojiLocalizer {
return variantKey => {
const parentKey = getEmojiParentKeyByVariantKey(variantKey);
const localeShortName = emojiLocalizerIndex.get(parentKey);
if (localeShortName != null) {
return localeShortName;
}
// Fallback to english short name
const parent = getEmojiParentByKey(parentKey);
return parent.englishShortNameDefault;
};
}
export function useFunEmojiLocalizer(): FunEmojiLocalizer {
const { emojiLocalizerIndex } = useFunEmojiLocalization();
const emojiLocalizer: FunEmojiLocalizer = useCallback(
variantKey => {
const parentKey = getEmojiParentKeyByVariantKey(variantKey);
const localeShortName = emojiLocalizerIndex.get(parentKey);
if (localeShortName != null) {
return localeShortName;
}
// Fallback to english short name
const parent = getEmojiParentByKey(parentKey);
return parent.englishShortNameDefault;
},
[emojiLocalizerIndex]
);
const emojiLocalizer = useMemo(() => {
return _createFunEmojiLocalizer(emojiLocalizerIndex);
}, [emojiLocalizerIndex]);
return emojiLocalizer;
}

View file

@ -8,6 +8,7 @@ import {
getEmojiParentByKey,
getEmojiParentKeyByValue,
isEmojiParentValue,
normalizeShortNameCompletionQuery,
} from './data/emojis';
import type { LocaleEmojiListType } from '../../types/emoji';
import { useFunEmojiLocalization } from './FunEmojiLocalizationProvider';
@ -23,10 +24,14 @@ export type FunEmojiSearchIndexEntry = Readonly<{
export type FunEmojiSearchIndex = ReadonlyArray<FunEmojiSearchIndexEntry>;
export type FunEmojiSearchResult = Readonly<{
parentKey: EmojiParentKey;
}>;
export type FunEmojiSearch = (
query: string,
limit?: number
) => ReadonlyArray<EmojiParentKey>;
) => ReadonlyArray<FunEmojiSearchResult>;
export function createFunEmojiSearchIndex(
localeEmojiList: LocaleEmojiListType
@ -44,8 +49,10 @@ export function createFunEmojiSearchIndex(
results.push({
key: parentKey,
rank: localeEmoji.rank,
shortName: localeEmoji.shortName,
shortNames: localeEmoji.tags,
shortName: normalizeShortNameCompletionQuery(localeEmoji.shortName),
shortNames: localeEmoji.tags.map(tag => {
return normalizeShortNameCompletionQuery(tag);
}),
emoticon: emoji.emoticonDefault,
emoticons: emoji.emoticons,
});
@ -67,6 +74,7 @@ const FuseFuzzyOptions: Fuse.IFuseOptions<FunEmojiSearchIndexEntry> = {
minMatchCharLength: 1,
keys: FuseKeys,
includeScore: true,
includeMatches: true,
};
const FuseExactOptions: Fuse.IFuseOptions<FunEmojiSearchIndexEntry> = {
@ -75,49 +83,48 @@ const FuseExactOptions: Fuse.IFuseOptions<FunEmojiSearchIndexEntry> = {
minMatchCharLength: 1,
keys: FuseKeys,
includeScore: true,
includeMatches: true,
};
function createFunEmojiSearch(
/** @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(query, limit = 200) {
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), {
limit: limit * 2,
});
const rawResults = fuse.search(query.substring(0, 32));
const rankedResults = rawResults.map(result => {
// 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 (result.item.shortName.startsWith(query)) {
return {
score: result.item.shortName.length / query.length,
item: result.item,
};
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 {
score: 1 + (result.score ?? 0) + rank / emojiSearchIndex.length,
item: result.item,
};
return 1 + queryScore + rankScore;
});
const sortedResults = sortBy(rankedResults, result => {
return result.score;
});
const truncatedResults = sortedResults.slice(0, limit);
return truncatedResults.map(result => {
return result.item.key;
return sortedResults.slice(0, limit).map(result => {
return { parentKey: result.item.key };
});
};
}
@ -125,7 +132,7 @@ function createFunEmojiSearch(
export function useFunEmojiSearch(): FunEmojiSearch {
const { emojiSearchIndex } = useFunEmojiLocalization();
const emojiSearch = useMemo(() => {
return createFunEmojiSearch(emojiSearchIndex);
return _createFunEmojiSearch(emojiSearchIndex);
}, [emojiSearchIndex]);
return emojiSearch;
}

View file

@ -1,62 +0,0 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useEffect, useCallback, useRef } from 'react';
import { data, createSearch } from '../components/emoji/lib';
import type { SearchEmojiListType } from '../components/emoji/lib';
import { drop } from '../util/drop';
import * as log from '../logging/log';
const uninitialized: SearchEmojiListType = data.map(
({ short_name: shortName, short_names: shortNames }) => {
return {
shortName,
rank: 0,
tags: shortNames,
};
}
);
const defaultSearch = createSearch(uninitialized);
export function useEmojiSearch(
locale: string
): ReturnType<typeof createSearch> {
const searchRef = useRef(defaultSearch);
useEffect(() => {
let canceled = false;
async function run() {
let result: SearchEmojiListType | undefined;
try {
result = await window.SignalContext.getLocalizedEmojiList(locale);
} catch (error) {
log.error(`Failed to get localized emoji list for ${locale}`, error);
}
// Fallback
if (result === undefined) {
try {
result = await window.SignalContext.getLocalizedEmojiList('en');
} catch (error) {
log.error('Failed to get fallback localized emoji list');
}
}
if (!canceled && result !== undefined) {
searchRef.current = createSearch(result);
}
}
drop(run());
return () => {
canceled = true;
};
}, [locale]);
return useCallback((...args) => {
return searchRef.current?.(...args);
}, []);
}

View file

@ -12,25 +12,27 @@ import classNames from 'classnames';
import { createPortal } from 'react-dom';
import type { VirtualElement } from '@popperjs/core';
import { convertShortName, isShortName } from '../../components/emoji/lib';
import type { SearchFnType } from '../../components/emoji/lib';
import type { EmojiPickDataType } from '../../components/emoji/EmojiPicker';
import { getBlotTextPartitions, matchBlotTextPartitions } from '../util';
import { handleOutsideClick } from '../../util/handleOutsideClick';
import * as log from '../../logging/log';
import { FunStaticEmoji } from '../../components/fun/FunEmoji';
import { strictAssert } from '../../util/assert';
import {
EmojiSkinTone,
getEmojiParentKeyByEnglishShortName,
getEmojiParentByKey,
getEmojiVariantByParentKeyAndSkinTone,
isEmojiEnglishShortName,
normalizeShortNameCompletionDisplay,
} from '../../components/fun/data/emojis';
import type { FunEmojiSearchResult } from '../../components/fun/useFunEmojiSearch';
import { type FunEmojiSearch } from '../../components/fun/useFunEmojiSearch';
import { type FunEmojiLocalizer } from '../../components/fun/useFunEmojiLocalizer';
export type EmojiCompletionOptions = {
onPickEmoji: (emoji: EmojiPickDataType) => void;
setEmojiPickerElement: (element: JSX.Element | null) => void;
emojiSkinToneDefault: EmojiSkinTone | null;
search: SearchFnType;
emojiSearch: FunEmojiSearch;
emojiLocalizer: FunEmojiLocalizer;
};
export type InsertEmojiOptionsType = Readonly<{
@ -42,7 +44,7 @@ export type InsertEmojiOptionsType = Readonly<{
}>;
export class EmojiCompletion {
results: Array<string>;
results: ReadonlyArray<FunEmojiSearchResult>;
index: number;
@ -197,7 +199,7 @@ export class EmojiCompletion {
return PASS_THROUGH;
}
const showEmojiResults = this.options.search(leftTokenText, 10);
const showEmojiResults = this.options.emojiSearch(leftTokenText, 10);
if (showEmojiResults.length > 0) {
this.results = showEmojiResults;
@ -229,7 +231,7 @@ export class EmojiCompletion {
return;
}
const emoji = this.results[this.index];
const result = this.results[this.index];
const [leafText] = this.getCurrentLeafTextPartitions();
const tokenTextMatch = /:([-+0-9\p{Alpha}_]*)(:?)$/iu.exec(leafText);
@ -239,9 +241,10 @@ export class EmojiCompletion {
}
const [, tokenText] = tokenTextMatch;
const parent = getEmojiParentByKey(result.parentKey);
this.insertEmoji({
shortName: emoji,
shortName: parent.englishShortNameDefault,
index: range.index - tokenText.length - 1,
range: tokenText.length + 1,
withTrailingSpace: true,
@ -362,26 +365,30 @@ export class EmojiCompletion {
role="listbox"
aria-expanded
aria-activedescendant={`emoji-result--${
emojiResults.length ? emojiResults[emojiResultsIndex] : ''
emojiResults.length
? emojiResults[emojiResultsIndex].parentKey
: ''
}`}
tabIndex={0}
>
{emojiResults.map((emoji, index) => {
strictAssert(
isEmojiEnglishShortName(emoji),
'Must be valid english short name'
);
const emojiParentKey = getEmojiParentKeyByEnglishShortName(emoji);
{emojiResults.map((result, index) => {
const emojiVariant = getEmojiVariantByParentKeyAndSkinTone(
emojiParentKey,
result.parentKey,
this.options.emojiSkinToneDefault ?? EmojiSkinTone.None
);
const localeShortName = this.options.emojiLocalizer(
emojiVariant.key
);
const normalized =
normalizeShortNameCompletionDisplay(localeShortName);
return (
<button
type="button"
key={emoji}
id={`emoji-result--${emoji}`}
key={result.parentKey}
id={`emoji-result--${result.parentKey}`}
role="option button"
aria-selected={emojiResultsIndex === index}
onClick={() => {
@ -401,7 +408,7 @@ export class EmojiCompletion {
size={16}
/>
<div className="module-composition-input__suggestions__row__short-name">
:{emoji}:
:{normalized}:
</div>
</button>
);

View file

@ -9,8 +9,53 @@ import type {
EmojiCompletionOptions,
InsertEmojiOptionsType,
} from '../../../quill/emoji/completion';
import { createSearch } from '../../../components/emoji/lib';
import { EmojiSkinTone } from '../../../components/fun/data/emojis';
import {
EmojiSkinTone,
emojiVariantConstant,
getEmojiParentKeyByVariantKey,
} from '../../../components/fun/data/emojis';
import {
_createFunEmojiSearch,
createFunEmojiSearchIndex,
} from '../../../components/fun/useFunEmojiSearch';
import {
_createFunEmojiLocalizer,
createFunEmojiLocalizerIndex,
} from '../../../components/fun/useFunEmojiLocalizer';
import type { LocaleEmojiListType } from '../../../types/emoji';
const EMOJI_VARIANTS = {
SMILE: emojiVariantConstant('\u{1F604}'),
SMILE_CAT: emojiVariantConstant('\u{1F638}'),
FRIEND_SHRIMP: emojiVariantConstant('\u{1F364}'),
} as const;
const PARENT_KEYS = {
SMILE: getEmojiParentKeyByVariantKey(EMOJI_VARIANTS.SMILE.key),
SMILE_CAT: getEmojiParentKeyByVariantKey(EMOJI_VARIANTS.SMILE_CAT.key),
FRIED_SHRIMP: getEmojiParentKeyByVariantKey(EMOJI_VARIANTS.FRIEND_SHRIMP.key),
} as const;
const EMOJI_LIST: LocaleEmojiListType = [
{
emoji: EMOJI_VARIANTS.SMILE.value,
shortName: 'smile',
tags: [],
rank: 0,
},
{
emoji: EMOJI_VARIANTS.SMILE_CAT.value,
shortName: 'smile_cat',
tags: [],
rank: 0,
},
{
emoji: EMOJI_VARIANTS.FRIEND_SHRIMP.value,
shortName: 'fried_shrimp',
tags: [],
rank: 0,
},
];
describe('emojiCompletion', () => {
let emojiCompletion: EmojiCompletion;
@ -29,14 +74,19 @@ describe('emojiCompletion', () => {
setSelection: sinon.stub(),
updateContents: sinon.stub(),
};
const searchIndex = createFunEmojiSearchIndex(EMOJI_LIST);
const localizerIndex = createFunEmojiLocalizerIndex(EMOJI_LIST);
const emojiSearch = _createFunEmojiSearch(searchIndex);
const emojiLocalizer = _createFunEmojiLocalizer(localizerIndex);
const options: EmojiCompletionOptions = {
onPickEmoji: sinon.stub(),
setEmojiPickerElement: sinon.stub(),
emojiSkinToneDefault: EmojiSkinTone.None,
search: createSearch([
{ shortName: 'smile', tags: [], rank: 0 },
{ shortName: 'smile_cat', tags: [], rank: 0 },
]),
emojiSearch,
emojiLocalizer,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -64,7 +114,7 @@ describe('emojiCompletion', () => {
let insertEmojiStub: sinon.SinonStub<[InsertEmojiOptionsType], void>;
beforeEach(() => {
emojiCompletion.results = ['joy'];
emojiCompletion.results = [{ parentKey: PARENT_KEYS.SMILE }];
emojiCompletion.index = 5;
insertEmojiStub = sinon
.stub(emojiCompletion, 'insertEmoji')
@ -171,7 +221,7 @@ describe('emojiCompletion', () => {
});
it('stores the results and renders', () => {
assert.equal(emojiCompletion.results.length, 10);
assert.equal(emojiCompletion.results.length, 2);
assert.equal((emojiCompletion.render as sinon.SinonStub).called, true);
});
});
@ -353,7 +403,10 @@ describe('emojiCompletion', () => {
let insertEmojiStub: sinon.SinonStub<[InsertEmojiOptionsType], void>;
beforeEach(() => {
emojiCompletion.results = ['smile', 'smile_cat'];
emojiCompletion.results = [
{ parentKey: PARENT_KEYS.SMILE },
{ parentKey: PARENT_KEYS.SMILE_CAT },
];
emojiCompletion.index = 1;
insertEmojiStub = sinon.stub(emojiCompletion, 'insertEmoji');
});

View file

@ -1994,6 +1994,27 @@
"reasonCategory": "usageTrusted",
"updated": "2023-01-18T22:32:43.901Z"
},
{
"rule": "React-useRef",
"path": "ts/components/fun/FunGif.tsx",
"line": " const ref = useRef<HTMLVideoElement>(null);",
"reasonCategory": "usageTrusted",
"updated": "2025-03-21T23:22:07.920Z"
},
{
"rule": "React-useRef",
"path": "ts/components/fun/FunGif.tsx",
"line": " const timerRef = useRef<ReturnType<typeof setTimeout>>();",
"reasonCategory": "usageTrusted",
"updated": "2025-03-21T23:22:07.920Z"
},
{
"rule": "React-useRef",
"path": "ts/components/fun/FunGif.tsx",
"line": " const videoRef = useRef<HTMLVideoElement>(null);",
"reasonCategory": "usageTrusted",
"updated": "2025-03-24T18:57:50.198Z"
},
{
"rule": "React-useRef",
"path": "ts/components/fun/base/FunImage.tsx",
@ -2127,27 +2148,6 @@
"reasonCategory": "usageTrusted",
"updated": "2025-02-19T20:14:46.879Z"
},
{
"rule": "React-useRef",
"path": "ts/components/fun/FunGif.tsx",
"line": " const ref = useRef<HTMLVideoElement>(null);",
"reasonCategory": "usageTrusted",
"updated": "2025-03-21T23:22:07.920Z"
},
{
"rule": "React-useRef",
"path": "ts/components/fun/FunGif.tsx",
"line": " const timerRef = useRef<ReturnType<typeof setTimeout>>();",
"reasonCategory": "usageTrusted",
"updated": "2025-03-21T23:22:07.920Z"
},
{
"rule": "React-useRef",
"path": "ts/components/fun/FunGif.tsx",
"line": " const videoRef = useRef<HTMLVideoElement>(null);",
"reasonCategory": "usageTrusted",
"updated": "2025-03-24T18:57:50.198Z"
},
{
"rule": "React-useRef",
"path": "ts/components/stickers/StickerButton.tsx",
@ -2156,14 +2156,6 @@
"updated": "2022-06-14T22:04:43.988Z",
"reasonDetail": "Handling outside click"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useEmojiSearch.ts",
"line": " const searchRef = useRef(defaultSearch);",
"reasonCategory": "usageTrusted",
"updated": "2024-03-16T18:34:38.165Z",
"reasonDetail": "Quill requires an immutable reference to the search function"
},
{
"rule": "React-useRef",
"path": "ts/hooks/useIntersectionObserver.ts",