aboutsummaryrefslogtreecommitdiffstats
path: root/web_src/js
diff options
context:
space:
mode:
authorYarden Shoham <git@yardenshoham.com>2023-04-22 18:32:34 +0300
committerGitHub <noreply@github.com>2023-04-22 11:32:34 -0400
commit3cc87370c3b4495edf9f31a9f96e2b6f34cbd35d (patch)
treeda483845a6d12486eb3bc2704ab730a84ed20cea /web_src/js
parentce9c1ddc4c31ef6f4447111e2e187ea54e1a195a (diff)
downloadgitea-3cc87370c3b4495edf9f31a9f96e2b6f34cbd35d.tar.gz
gitea-3cc87370c3b4495edf9f31a9f96e2b6f34cbd35d.zip
Improve emoji and mention matching (#24255)
Prioritize matches that start with the given text, then matches that contain the given text. I wanted to add a heart emoji on a pull request comment so I started writing `:`, `h`, `e`, `a`, `r` (at this point I still couldn't find the heart), `t`... The heart was not on the list, that's weird - it feels like I made a typo or a mistake. This fixes that. This also feels more like GitHub's emoji auto-complete. # Before ![image](https://user-images.githubusercontent.com/20454870/233630750-bd0a1b76-33d0-41d4-9218-a37b670c42b0.png) # After ![image](https://user-images.githubusercontent.com/20454870/233775128-05e67fc1-e092-4025-b6f7-1fd8e5f71e87.png) --------- Signed-off-by: Yarden Shoham <git@yardenshoham.com> Co-authored-by: silverwind <me@silverwind.io>
Diffstat (limited to 'web_src/js')
-rw-r--r--web_src/js/features/comp/ComboMarkdownEditor.js22
-rw-r--r--web_src/js/test/setup.js9
-rw-r--r--web_src/js/utils/match.js43
-rw-r--r--web_src/js/utils/match.test.js47
4 files changed, 103 insertions, 18 deletions
diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js
index eb73b0914d..9995033e89 100644
--- a/web_src/js/features/comp/ComboMarkdownEditor.js
+++ b/web_src/js/features/comp/ComboMarkdownEditor.js
@@ -5,11 +5,11 @@ import {attachTribute} from '../tribute.js';
import {hideElem, showElem, autosize} from '../../utils/dom.js';
import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js';
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
-import {emojiKeys, emojiString} from '../emoji.js';
+import {emojiString} from '../emoji.js';
import {renderPreviewPanelContent} from '../repo-editor.js';
+import {matchEmoji, matchMention} from '../../utils/match.js';
let elementIdCounter = 0;
-const maxExpanderMatches = 6;
/**
* validate if the given textarea is non-empty.
@@ -106,14 +106,7 @@ class ComboMarkdownEditor {
const expander = this.container.querySelector('text-expander');
expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => {
if (key === ':') {
- const matches = [];
- const textLowerCase = text.toLowerCase();
- for (const name of emojiKeys) {
- if (name.toLowerCase().includes(textLowerCase)) {
- matches.push(name);
- if (matches.length >= maxExpanderMatches) break;
- }
- }
+ const matches = matchEmoji(text);
if (!matches.length) return provide({matched: false});
const ul = document.createElement('ul');
@@ -129,14 +122,7 @@ class ComboMarkdownEditor {
provide({matched: true, fragment: ul});
} else if (key === '@') {
- const matches = [];
- const textLowerCase = text.toLowerCase();
- for (const obj of window.config.tributeValues) {
- if (obj.key.toLowerCase().includes(textLowerCase)) {
- matches.push(obj);
- if (matches.length >= maxExpanderMatches) break;
- }
- }
+ const matches = matchMention(text);
if (!matches.length) return provide({matched: false});
const ul = document.createElement('ul');
diff --git a/web_src/js/test/setup.js b/web_src/js/test/setup.js
index e0e2c71e29..d9f0b8b547 100644
--- a/web_src/js/test/setup.js
+++ b/web_src/js/test/setup.js
@@ -3,4 +3,13 @@ window.config = {
pageData: {},
i18n: {},
appSubUrl: '',
+ tributeValues: [
+ {key: 'user1 User 1', value: 'user1', name: 'user1', fullname: 'User 1', avatar: 'https://avatar1.com'},
+ {key: 'user2 User 2', value: 'user2', name: 'user2', fullname: 'User 2', avatar: 'https://avatar2.com'},
+ {key: 'user3 User 3', value: 'user3', name: 'user3', fullname: 'User 3', avatar: 'https://avatar3.com'},
+ {key: 'user4 User 4', value: 'user4', name: 'user4', fullname: 'User 4', avatar: 'https://avatar4.com'},
+ {key: 'user5 User 5', value: 'user5', name: 'user5', fullname: 'User 5', avatar: 'https://avatar5.com'},
+ {key: 'user6 User 6', value: 'user6', name: 'user6', fullname: 'User 6', avatar: 'https://avatar6.com'},
+ {key: 'user7 User 7', value: 'user7', name: 'user7', fullname: 'User 7', avatar: 'https://avatar7.com'},
+ ],
};
diff --git a/web_src/js/utils/match.js b/web_src/js/utils/match.js
new file mode 100644
index 0000000000..0d20ca336f
--- /dev/null
+++ b/web_src/js/utils/match.js
@@ -0,0 +1,43 @@
+import emojis from '../../../assets/emoji.json';
+
+const maxMatches = 6;
+
+function sortAndReduce(map) {
+ const sortedMap = new Map([...map.entries()].sort((a, b) => a[1] - b[1]));
+ return Array.from(sortedMap.keys()).slice(0, maxMatches);
+}
+
+export function matchEmoji(queryText) {
+ const query = queryText.toLowerCase().replaceAll('_', ' ');
+ if (!query) return emojis.slice(0, maxMatches).map((e) => e.aliases[0]);
+
+ // results is a map of weights, lower is better
+ const results = new Map();
+ for (const {aliases} of emojis) {
+ const mainAlias = aliases[0];
+ for (const [aliasIndex, alias] of aliases.entries()) {
+ const index = alias.replaceAll('_', ' ').indexOf(query);
+ if (index === -1) continue;
+ const existing = results.get(mainAlias);
+ const rankedIndex = index + aliasIndex;
+ results.set(mainAlias, existing ? existing - rankedIndex : rankedIndex);
+ }
+ }
+
+ return sortAndReduce(results);
+}
+
+export function matchMention(queryText) {
+ const query = queryText.toLowerCase();
+
+ // results is a map of weights, lower is better
+ const results = new Map();
+ for (const obj of window.config.tributeValues) {
+ const index = obj.key.toLowerCase().indexOf(query);
+ if (index === -1) continue;
+ const existing = results.get(obj);
+ results.set(obj, existing ? existing - index : index);
+ }
+
+ return sortAndReduce(results);
+}
diff --git a/web_src/js/utils/match.test.js b/web_src/js/utils/match.test.js
new file mode 100644
index 0000000000..78710f2a5f
--- /dev/null
+++ b/web_src/js/utils/match.test.js
@@ -0,0 +1,47 @@
+import {test, expect} from 'vitest';
+import {matchEmoji, matchMention} from './match.js';
+
+test('matchEmoji', () => {
+ expect(matchEmoji('')).toEqual([
+ '+1',
+ '-1',
+ '100',
+ '1234',
+ '1st_place_medal',
+ '2nd_place_medal',
+ ]);
+
+ expect(matchEmoji('hea')).toEqual([
+ 'headphones',
+ 'headstone',
+ 'health_worker',
+ 'hear_no_evil',
+ 'heard_mcdonald_islands',
+ 'heart',
+ ]);
+
+ expect(matchEmoji('hear')).toEqual([
+ 'hear_no_evil',
+ 'heard_mcdonald_islands',
+ 'heart',
+ 'heart_decoration',
+ 'heart_eyes',
+ 'heart_eyes_cat',
+ ]);
+
+ expect(matchEmoji('poo')).toEqual([
+ 'poodle',
+ 'hankey',
+ 'spoon',
+ 'bowl_with_spoon',
+ ]);
+
+ expect(matchEmoji('1st_')).toEqual([
+ '1st_place_medal',
+ ]);
+});
+
+test('matchMention', () => {
+ expect(matchMention('')).toEqual(window.config.tributeValues.slice(0, 6));
+ expect(matchMention('user4')).toEqual([window.config.tributeValues[3]]);
+});