Skip to content

Improve emoji and mention matching #24255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 22, 2023
Merged
22 changes: 4 additions & 18 deletions web_src/js/features/comp/ComboMarkdownEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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');
Expand All @@ -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');
Expand Down
9 changes: 9 additions & 0 deletions web_src/js/test/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
],
};
43 changes: 43 additions & 0 deletions web_src/js/utils/match.js
Original file line number Diff line number Diff line change
@@ -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);
}
47 changes: 47 additions & 0 deletions web_src/js/utils/match.test.js
Original file line number Diff line number Diff line change
@@ -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]]);
});