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 f32a65f62..270a83975 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; @@ -126,145 +128,41 @@ const searchResultCustom = ref([]); const searchResultUnicode = ref([]); const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index'); -watch(q, () => { - if (emojis.value) emojis.value.scrollTop = 0; +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); +} - if (q.value == null || q.value === '') { +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 === '') { 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()); + queryTimeoutId = setTimeout(queryCallback, 300, 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',