From ded328fb43adda0ca61946de1ce1e94bc8e37b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Thu, 23 Nov 2023 08:13:51 +0900 Subject: [PATCH] =?UTF-8?q?=E7=B5=B5=E6=96=87=E5=AD=97=E3=81=AE=E3=82=AA?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=82=B3=E3=83=B3=E3=83=97=E3=83=AA=E3=83=BC?= =?UTF-8?q?=E3=83=88=E5=BC=B7=E5=8C=96=E3=81=AE=E5=AF=BE=E5=BF=9C=20(#1236?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 前方一致・部分一致でなくても近似値でヒットするように * fix CHANGELOG.md * for of に変更 --------- Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com> --- CHANGELOG.md | 1 + .../src/components/MkAutocomplete.vue | 100 ++++++++++++++---- 2 files changed, 78 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 111337459..bc39e423c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) ### Client +- Enhance: 絵文字のオートコンプリート機能強化 #12364 - fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正 - Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367 diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 7e0c21904..a0f496111 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -242,29 +242,7 @@ function exec() { return; } - const matched: EmojiDef[] = []; - const max = 30; - - emojiDb.value.some(x => { - if (x.name.startsWith(props.q ?? '') && !x.aliasOf && !matched.some(y => y.emoji === x.emoji)) matched.push(x); - return matched.length === max; - }); - - if (matched.length < max) { - emojiDb.value.some(x => { - if (x.name.startsWith(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x); - return matched.length === max; - }); - } - - if (matched.length < max) { - emojiDb.value.some(x => { - if (x.name.includes(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x); - return matched.length === max; - }); - } - - emojis.value = matched; + emojis.value = emojiAutoComplete(props.q, emojiDb.value); } else if (props.type === 'mfmTag') { if (!props.q || props.q === '') { mfmTags.value = MFM_TAGS; @@ -275,6 +253,82 @@ function exec() { } } +type EmojiScore = { emoji: EmojiDef, score: number }; + +function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] { + if (!query) { + return []; + } + + const matched = new Map(); + + // 前方一致(エイリアスなし) + emojiDb.some(x => { + if (x.name.startsWith(query) && !x.aliasOf) { + matched.set(x.name, { emoji: x, score: query.length }); + } + return matched.size === max; + }); + + // 前方一致(エイリアス込み) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name.startsWith(query)) { + matched.set(x.name, { emoji: x, score: query.length }); + } + return matched.size === max; + }); + } + + // 部分一致(エイリアス込み) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name.includes(query)) { + matched.set(x.name, { emoji: x, score: query.length }); + } + return matched.size === max; + }); + } + + // 簡易あいまい検索 + if (matched.size < max) { + const queryChars = [...query]; + const hitEmojis = new Map(); + + for (const x of emojiDb) { + // クエリ文字列の1文字単位で絵文字名にヒットするかを見る + // ただし、過剰に検出されるのを防ぐためクエリ文字列に登場する順番で絵文字名を走査する + + let queryCharHitPos = 0; + let queryCharHitCount = 0; + for (let idx = 0; idx < queryChars.length; idx++) { + queryCharHitPos = x.name.indexOf(queryChars[idx], queryCharHitPos); + if (queryCharHitPos <= -1) { + break; + } + + queryCharHitCount++; + } + + // ヒット数が少なすぎると検索結果が汚れるので調節する + if (queryCharHitCount > 2) { + hitEmojis.set(x.name, { emoji: x, score: queryCharHitCount }); + } + } + + // ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分) + [...hitEmojis.values()] + .sort((x, y) => y.score - x.score) + .slice(0, 6) + .forEach(it => matched.set(it.emoji.name, it)); + } + + return [...matched.values()] + .sort((x, y) => y.score - x.score) + .slice(0, max) + .map(it => it.emoji); +} + function onMousedown(event: Event) { if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close(); }