From 538e81db56a5ca9c8d7ddd288ae2dcd8066c13ab Mon Sep 17 00:00:00 2001 From: Chloe Kudryavtsev Date: Thu, 1 Sep 2022 08:21:43 -0400 Subject: [PATCH 1/3] client: optimize, simplify and smartify emoji picker search The query is split up on spaces, and we search for each of those terms, in order, anywhere in the emoji name or any aliases/keywords. This is done in a single filter pass against a compiled regex, making the process reasonably performant. Based on rough estimates, it should be between 2 and 5x faster than the old implementation, depending on several factors. There is a natural space left in to sort by relevancy (not done yet). It should also be easy to make the number of matches shown configurable. The number of matches is relevant, especially pre-sort. Another consideration is to delay the calculation by up to 300ms. --- .../client/src/components/emoji-picker.vue | 151 +++--------------- 1 file changed, 19 insertions(+), 132 deletions(-) diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue index f32a65f62..03ba29184 100644 --- a/packages/client/src/components/emoji-picker.vue +++ b/packages/client/src/components/emoji-picker.vue @@ -126,145 +126,32 @@ const searchResultCustom = ref([]); const searchResultUnicode = ref([]); const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index'); +function emojiSearch(src: Type[], max: number, query: string): Type[] { + // discount fuzzy matching pattern + const re = new RegExp(query.split(' ').join('.*'), 'i'); + const match = (str: string): boolean => str && re.test(str); + const matches = src.filter(emoji => + match(emoji.name) + || emoji.aliases?.some(match) // custom emoji + || emoji.keywords?.some(match) // unicode emoji + ); + // TODO: sort matches by distance to query + if (max <= 0 || matches.length < max) return matches; + return matches.slice(0, max); +} + watch(q, () => { if (emojis.value) emojis.value.scrollTop = 0; - if (q.value == null || q.value === '') { + const query = q.value; + if (query == null || query === '') { searchResultCustom.value = []; searchResultUnicode.value = []; return; } - - const newQ = q.value.replace(/:/g, '').toLowerCase(); - - const searchCustom = () => { - const max = 8; - const emojis = customEmojis; - const matches = new Set(); - - const exactMatch = emojis.find(emoji => emoji.name === newQ); - if (exactMatch) matches.add(exactMatch); - - if (newQ.includes(' ')) { // AND検索 - const keywords = newQ.split(' '); - - // 名前にキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - // 名前またはエイリアスにキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - } else { - for (const emoji of emojis) { - if (emoji.name.startsWith(newQ)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.aliases.some(alias => alias.startsWith(newQ))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.name.includes(newQ)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.aliases.some(alias => alias.includes(newQ))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - } - - return matches; - }; - - const searchUnicode = () => { - const max = 8; - const emojis = emojilist; - const matches = new Set(); - - const exactMatch = emojis.find(emoji => emoji.name === newQ); - if (exactMatch) matches.add(exactMatch); - - if (newQ.includes(' ')) { // AND検索 - const keywords = newQ.split(' '); - - // 名前にキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - // 名前またはエイリアスにキーワードが含まれている - for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - } else { - for (const emoji of emojis) { - if (emoji.name.startsWith(newQ)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.name.includes(newQ)) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - if (matches.size >= max) return matches; - - for (const emoji of emojis) { - if (emoji.keywords.some(keyword => keyword.includes(newQ))) { - matches.add(emoji); - if (matches.size >= max) break; - } - } - } - - return matches; - }; - - searchResultCustom.value = Array.from(searchCustom()); - searchResultUnicode.value = Array.from(searchUnicode()); + + searchResultCustom.value = emojiSearch(instance.emojis, 10, query); + searchResultUnicode.value = emojiSearch(emojilist, 10, query); }); function focus() { From 33ed6e98a744cce4d1393ccb2dd7aa14445d407d Mon Sep 17 00:00:00 2001 From: Chloe Kudryavtsev Date: Thu, 1 Sep 2022 13:20:20 -0400 Subject: [PATCH 2/3] client: make emoji picker suggestion count configurable --- locales/en-US.yml | 4 ++++ packages/client/src/components/emoji-picker.vue | 6 ++++-- packages/client/src/pages/settings/general.vue | 11 +++++++++++ packages/client/src/store.ts | 8 ++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index 7038dab5c..724e75cd0 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -987,6 +987,10 @@ _serverDisconnectedBehavior: reload: "Automatically reload" dialog: "Show warning dialog" quiet: "Show unobtrusive warning" +maxCustomEmojiPicker: "Maximum suggested custom emoji in picker" +maxCustomEmojiPickerDescription: "0 for unlimited" +maxUnicodeEmojiPicker: "Maximum suggested unicode emoji in picker" +maxUnicodeEmojiPickerDescription: "0 for unlimited" _channel: create: "Create channel" edit: "Edit channel" diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue index 03ba29184..567715f16 100644 --- a/packages/client/src/components/emoji-picker.vue +++ b/packages/client/src/components/emoji-picker.vue @@ -112,6 +112,8 @@ const { reactionPickerSize, reactionPickerWidth, reactionPickerHeight, + maxCustomEmojiPicker, + maxUnicodeEmojiPicker, disableShowingAnimatedImages, recentlyUsedEmojis, } = defaultStore.reactiveState; @@ -150,8 +152,8 @@ watch(q, () => { return; } - searchResultCustom.value = emojiSearch(instance.emojis, 10, query); - searchResultUnicode.value = emojiSearch(emojilist, 10, query); + searchResultCustom.value = emojiSearch(instance.emojis, maxCustomEmojiPicker.value, query); + searchResultUnicode.value = emojiSearch(emojilist, maxUnicodeEmojiPicker.value, query); }); function focus() { diff --git a/packages/client/src/pages/settings/general.vue b/packages/client/src/pages/settings/general.vue index 74fa0bc92..9be40bdd8 100644 --- a/packages/client/src/pages/settings/general.vue +++ b/packages/client/src/pages/settings/general.vue @@ -35,6 +35,15 @@ + + + + + + + + + @@ -124,6 +133,8 @@ async function reloadAsk() { const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind')); const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior')); +const maxCustomEmojiPicker = computed(defaultStore.makeGetterSetter('maxCustomEmojiPicker')); +const maxUnicodeEmojiPicker = computed(defaultStore.makeGetterSetter('maxUnicodeEmojiPicker')); const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v)); const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal')); const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect')); diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index 6095e6f22..a592184fe 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -108,6 +108,14 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 'quiet' as 'quiet' | 'reload' | 'dialog', }, + maxCustomEmojiPicker: { + where: 'device', + default: 10, + }, + maxUnicodeEmojiPicker: { + where: 'device', + default: 10, + }, nsfw: { where: 'device', default: 'respect' as 'respect' | 'force' | 'ignore', From ed8e346ff9c3fb5a6b331e211e0b0e00c2d20dd8 Mon Sep 17 00:00:00 2001 From: Chloe Kudryavtsev Date: Thu, 1 Sep 2022 13:38:07 -0400 Subject: [PATCH 3/3] client: delay/batch emoji picker searches This is particularly important for users that set limit to 0 (unlimited). --- packages/client/src/components/emoji-picker.vue | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue index 567715f16..270a83975 100644 --- a/packages/client/src/components/emoji-picker.vue +++ b/packages/client/src/components/emoji-picker.vue @@ -142,8 +142,18 @@ function emojiSearch(src: Type[], max: number, query: string): Type[] { return matches.slice(0, max); } -watch(q, () => { +let queryTimeoutId = -1; +const queryCallback = (query) => { if (emojis.value) emojis.value.scrollTop = 0; + searchResultCustom.value = emojiSearch(instance.emojis, maxCustomEmojiPicker.value, query); + searchResultUnicode.value = emojiSearch(emojilist, maxUnicodeEmojiPicker.value, query); + queryTimeoutId = -1; +} +watch(q, () => { + if(queryTimeoutId >= 0) { + clearTimeout(queryTimeoutId); + queryTimeoutId = -1; + } const query = q.value; if (query == null || query === '') { @@ -151,9 +161,8 @@ watch(q, () => { searchResultUnicode.value = []; return; } - - searchResultCustom.value = emojiSearch(instance.emojis, maxCustomEmojiPicker.value, query); - searchResultUnicode.value = emojiSearch(emojilist, maxUnicodeEmojiPicker.value, query); + + queryTimeoutId = setTimeout(queryCallback, 300, query); }); function focus() {