Add localized emoji search
This commit is contained in:
parent
ce0fb22041
commit
e90553b3b3
17 changed files with 878 additions and 97 deletions
|
@ -52,6 +52,7 @@ import { isNotNil } from '../util/isNotNil';
|
|||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
import { useRefMerger } from '../hooks/useRefMerger';
|
||||
import { useEmojiSearch } from '../hooks/useEmojiSearch';
|
||||
import type { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
|
||||
import type { DraftEditMessageType } from '../model-types.d';
|
||||
|
@ -688,6 +689,8 @@ export function CompositionInput(props: Props): React.ReactElement {
|
|||
const callbacksRef = React.useRef(unstaleCallbacks);
|
||||
callbacksRef.current = unstaleCallbacks;
|
||||
|
||||
const search = useEmojiSearch(i18n.getLocale());
|
||||
|
||||
const reactQuill = React.useMemo(
|
||||
() => {
|
||||
const delta = generateDelta(draftText || '', draftBodyRanges || []);
|
||||
|
@ -739,6 +742,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
|||
onPickEmoji: (emoji: EmojiPickDataType) =>
|
||||
callbacksRef.current.onPickEmoji(emoji),
|
||||
skinTone,
|
||||
search,
|
||||
},
|
||||
autoSubstituteAsciiEmojis: {
|
||||
skinTone,
|
||||
|
|
|
@ -21,10 +21,11 @@ import {
|
|||
import FocusTrap from 'focus-trap-react';
|
||||
|
||||
import { Emoji } from './Emoji';
|
||||
import { dataByCategory, search } from './lib';
|
||||
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';
|
||||
|
||||
export type EmojiPickDataType = {
|
||||
skinTone?: number;
|
||||
|
@ -108,6 +109,8 @@ export const EmojiPicker = React.memo(
|
|||
const [scrollToRow, setScrollToRow] = React.useState(0);
|
||||
const [selectedTone, setSelectedTone] = React.useState(skinTone);
|
||||
|
||||
const search = useEmojiSearch(i18n.getLocale());
|
||||
|
||||
const handleToggleSearch = React.useCallback(
|
||||
(
|
||||
e:
|
||||
|
@ -261,10 +264,7 @@ export const EmojiPicker = React.memo(
|
|||
|
||||
const emojiGrid = React.useMemo(() => {
|
||||
if (searchText) {
|
||||
return chunk(
|
||||
search(searchText).map(e => e.short_name),
|
||||
COL_COUNT
|
||||
);
|
||||
return chunk(search(searchText), COL_COUNT);
|
||||
}
|
||||
|
||||
const chunks = flatMap(renderableCategories, cat =>
|
||||
|
@ -275,7 +275,7 @@ export const EmojiPicker = React.memo(
|
|||
);
|
||||
|
||||
return [...chunk(firstRecent, COL_COUNT), ...chunks];
|
||||
}, [firstRecent, renderableCategories, searchText]);
|
||||
}, [firstRecent, renderableCategories, searchText, search]);
|
||||
|
||||
const rowCount = emojiGrid.length;
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import { getOwn } from '../../util/getOwn';
|
|||
import * as log from '../../logging/log';
|
||||
import { MINUTE } from '../../util/durations';
|
||||
import { drop } from '../../util/drop';
|
||||
import type { LocaleEmojiType } from '../../types/emoji';
|
||||
|
||||
export const skinTones = ['1F3FB', '1F3FC', '1F3FD', '1F3FE', '1F3FF'];
|
||||
|
||||
|
@ -218,34 +219,69 @@ export function getImagePath(
|
|||
return makeImagePath(emojiData.image);
|
||||
}
|
||||
|
||||
const fuse = new Fuse(data, {
|
||||
shouldSort: true,
|
||||
threshold: 0.2,
|
||||
minMatchCharLength: 1,
|
||||
keys: ['short_name', 'short_names'],
|
||||
});
|
||||
export type SearchFnType = (query: string, count?: number) => Array<string>;
|
||||
|
||||
const fuseExactPrefix = new Fuse(data, {
|
||||
shouldSort: true,
|
||||
threshold: 0, // effectively a prefix search
|
||||
minMatchCharLength: 2,
|
||||
keys: ['short_name', 'short_names'],
|
||||
});
|
||||
export type SearchEmojiListType = ReadonlyArray<
|
||||
Pick<LocaleEmojiType, 'shortName' | 'rank' | 'tags'>
|
||||
>;
|
||||
|
||||
export function search(query: string, count = 0): Array<EmojiData> {
|
||||
// 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;
|
||||
type CachedSearchFnType = Readonly<{
|
||||
localeEmoji: SearchEmojiListType;
|
||||
fn: SearchFnType;
|
||||
}>;
|
||||
|
||||
const results = fuseIndex
|
||||
.search(query.substr(0, 32))
|
||||
.map(result => result.item);
|
||||
let cachedSearchFn: CachedSearchFnType | undefined;
|
||||
|
||||
if (count) {
|
||||
return take(results, count);
|
||||
export function createSearch(localeEmoji: SearchEmojiListType): SearchFnType {
|
||||
if (cachedSearchFn && cachedSearchFn.localeEmoji === localeEmoji) {
|
||||
return cachedSearchFn.fn;
|
||||
}
|
||||
|
||||
return results;
|
||||
const fuse = new Fuse(localeEmoji, {
|
||||
shouldSort: true,
|
||||
threshold: 0.2,
|
||||
minMatchCharLength: 1,
|
||||
keys: ['shortName', 'tags'],
|
||||
includeScore: true,
|
||||
});
|
||||
|
||||
const fuseExactPrefix = new Fuse(localeEmoji, {
|
||||
shouldSort: true,
|
||||
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;
|
||||
|
||||
return {
|
||||
score: (entry.score ?? 0) + rank / localeEmoji.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([
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue