Sort short/exact emoji matches before longer incomplete matches (#10212)

* apply sort for exact match

* add tests for emoji provider

* apply filter in the emoji picker

* add tests

* revert cypress version

* put correct copyright

* fix eslint

* fix eslint

* add type

* fix cypress test

* fix tsc types issues

* add forgotten space...

---------

Co-authored-by: grimhilt <grimhilt@users.noreply.github.com>
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
This commit is contained in:
grimhilt
2023-02-27 18:09:15 +01:00
committed by GitHub
parent b9f61da7e6
commit 0546a11fd9
4 changed files with 97 additions and 17 deletions

View File

@@ -19,7 +19,7 @@ limitations under the License.
*/
import React from "react";
import { uniq, sortBy, ListIteratee } from "lodash";
import { uniq, sortBy, uniqBy, ListIteratee } from "lodash";
import EMOTICON_REGEX from "emojibase-regex/emoticon";
import { Room } from "matrix-js-sdk/src/models/room";
@@ -118,7 +118,7 @@ export default class EmojiProvider extends AutocompleteProvider {
// Do second match with shouldMatchWordsOnly in order to match against 'name'
completions = completions.concat(this.nameMatcher.match(matchedString));
let sorters: ListIteratee<ISortedEmoji>[] = [];
const sorters: ListIteratee<ISortedEmoji>[] = [];
// make sure that emoticons come first
sorters.push((c) => score(matchedString, c.emoji.emoticon || ""));
@@ -140,11 +140,27 @@ export default class EmojiProvider extends AutocompleteProvider {
completions = completions.slice(0, LIMIT);
// Do a second sort to place emoji matching with frequently used one on top
sorters = [];
const recentlyUsedAutocomplete: ISortedEmoji[] = [];
this.recentlyUsed.forEach((emoji) => {
sorters.push((c) => score(emoji.shortcodes[0], c.emoji.shortcodes[0]));
if (emoji.shortcodes[0].indexOf(trimmedMatch) === 0) {
recentlyUsedAutocomplete.push({ emoji: emoji, _orderBy: 0 });
}
});
completions = sortBy<ISortedEmoji>(uniq(completions), sorters);
//if there is an exact shortcode match in the frequently used emojis, it goes before everything
for (let i = 0; i < recentlyUsedAutocomplete.length; i++) {
if (recentlyUsedAutocomplete[i].emoji.shortcodes[0] === trimmedMatch) {
const exactMatchEmoji = recentlyUsedAutocomplete[i];
for (let j = i; j > 0; j--) {
recentlyUsedAutocomplete[j] = recentlyUsedAutocomplete[j - 1];
}
recentlyUsedAutocomplete[0] = exactMatchEmoji;
break;
}
}
completions = recentlyUsedAutocomplete.concat(completions);
completions = uniqBy(completions, "emoji");
return completions.map((c) => ({
completion: c.emoji.unicode,

View File

@@ -194,10 +194,30 @@ class EmojiPicker extends React.Component<IProps, IState> {
emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id];
}
emojis = emojis.filter((emoji) => this.emojiMatchesFilter(emoji, lcFilter));
emojis = emojis.sort((a, b) => {
const indexA = a.shortcodes[0].indexOf(lcFilter);
const indexB = b.shortcodes[0].indexOf(lcFilter);
// Prioritize emojis containing the filter in its shortcode
if (indexA == -1 || indexB == -1) {
return indexB - indexA;
}
// If both emojis start with the filter
// put the shorter emoji first
if (indexA == 0 && indexB == 0) {
return a.shortcodes[0].length - b.shortcodes[0].length;
}
// Prioritize emojis starting with the filter
return indexA - indexB;
});
this.memoizedDataByCategory[cat.id] = emojis;
cat.enabled = emojis.length > 0;
// The setState below doesn't re-render the header and we already have the refs for updateVisibility, so...
cat.ref.current.disabled = !cat.enabled;
if (cat.ref.current) {
cat.ref.current.disabled = !cat.enabled;
}
}
this.setState({ filter });
// Header underlines need to be updated, but updating requires knowing