From a3731f33926a70e612a06f9eac25028541cbe778 Mon Sep 17 00:00:00 2001 From: HellhoundSoftware Date: Thu, 5 Dec 2024 23:38:36 -0500 Subject: [PATCH 1/5] sort custom emoticons in picker by sortKey (#18) --- .../migration/1733435351051-add-emoji-sort-key.js | 11 +++++++++++ packages/backend/src/core/CustomEmojiService.ts | 4 ++++ .../backend/src/core/entities/EmojiEntityService.ts | 2 ++ packages/backend/src/models/Emoji.ts | 5 +++++ packages/backend/src/models/json-schema/emoji.ts | 8 ++++++++ .../ImportCustomEmojisProcessorService.ts | 1 + .../src/server/api/endpoints/admin/emoji/add.ts | 2 ++ .../src/server/api/endpoints/admin/emoji/copy.ts | 1 + .../src/server/api/endpoints/admin/emoji/list.ts | 4 ++++ .../src/server/api/endpoints/admin/emoji/update.ts | 2 ++ packages/frontend-embed/vite.config.local-dev.ts | 2 +- packages/frontend-embed/vite.config.ts | 1 + .../src/components/MkCustomEmojiDetailedDialog.vue | 7 +++++++ .../src/components/MkEmojiPicker.section.vue | 4 ++-- packages/frontend/src/components/MkEmojiPicker.vue | 4 ++-- packages/frontend/src/custom-emojis.ts | 13 +++++++++++++ packages/frontend/src/pages/emoji-edit-dialog.vue | 5 +++++ packages/frontend/vite.config.local-dev.ts | 2 +- packages/frontend/vite.config.ts | 1 + 19 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 packages/backend/migration/1733435351051-add-emoji-sort-key.js diff --git a/packages/backend/migration/1733435351051-add-emoji-sort-key.js b/packages/backend/migration/1733435351051-add-emoji-sort-key.js new file mode 100644 index 0000000000..7060434cde --- /dev/null +++ b/packages/backend/migration/1733435351051-add-emoji-sort-key.js @@ -0,0 +1,11 @@ +export class AddEmojiSortKey1733435351051 { + name = 'AddEmojiSortKey1733435351051' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" ADD "sortKey" character varying(64)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "sortKey"`); + } +} diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index cd906a72af..4958eb376a 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -76,6 +76,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive: boolean; localOnly: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][]; + sortKey: string | null; }, moderator?: MiUser): Promise { const emoji = await this.emojisRepository.insertOne({ id: this.idService.gen(), @@ -91,6 +92,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive: data.isSensitive, localOnly: data.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction, + sortKey: data.sortKey, }); if (data.host == null) { @@ -121,6 +123,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive?: boolean; localOnly?: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][]; + sortKey?: string | null; }, moderator?: MiUser): Promise { const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() }); @@ -138,6 +141,7 @@ export class CustomEmojiService implements OnApplicationShutdown { publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined, + sortKey: data.sortKey, }); this.localEmojisCache.refresh(); diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 841bd731c0..e3ec6b1dee 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -34,6 +34,7 @@ export class EmojiEntityService { localOnly: emoji.localOnly ? true : undefined, isSensitive: emoji.isSensitive ? true : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, + sortKey: emoji.sortKey, }; } @@ -62,6 +63,7 @@ export class EmojiEntityService { isSensitive: emoji.isSensitive, localOnly: emoji.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, + sortKey: emoji.sortKey, }; } diff --git a/packages/backend/src/models/Emoji.ts b/packages/backend/src/models/Emoji.ts index d62b6e9f6f..9ef570deb1 100644 --- a/packages/backend/src/models/Emoji.ts +++ b/packages/backend/src/models/Emoji.ts @@ -81,4 +81,9 @@ export class MiEmoji { array: true, length: 128, default: '{}', }) public roleIdsThatCanBeUsedThisEmojiAsReaction: string[]; + + @Column('varchar', { + length: 64, nullable: true, + }) + public sortKey: string | null; } diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index 62686ad5ae..11b78db1c5 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -44,6 +44,10 @@ export const packedEmojiSimpleSchema = { format: 'id', }, }, + sortKey: { + type: 'string', + optional: false, nullable: true, + }, }, } as const; @@ -102,5 +106,9 @@ export const packedEmojiDetailedSchema = { format: 'id', }, }, + sortKey: { + type: 'string', + optional: false, nullable: true, + }, }, } as const; diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 17ba71df3d..07df550114 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -105,6 +105,7 @@ export class ImportCustomEmojisProcessorService { isSensitive: emojiInfo.isSensitive, localOnly: emojiInfo.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: [], + sortKey: emojiInfo.sortKey?.normalize('NFC'), }); } catch (e) { if (e instanceof Error || typeof e === 'string') { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index b45a3c7156..6267f5b8dd 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -56,6 +56,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { type: 'string', } }, + sortKey: { type: 'string', nullable: true }, }, required: ['name', 'fileId'], } as const; @@ -91,6 +92,7 @@ export default class extends Endpoint { // eslint- isSensitive: ps.isSensitive ?? false, localOnly: ps.localOnly ?? false, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], + sortKey: ps.sortKey?.normalize('NFC') ?? null, }, me); return this.emojiEntityService.packDetailed(emoji); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index acd2494131..8ae2e85275 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -97,6 +97,7 @@ export default class extends Endpoint { // eslint- isSensitive: emoji.isSensitive, localOnly: emoji.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, + sortKey: emoji.sortKey?.normalize('NFC') ?? null, }, me); return this.emojiEntityService.packDetailed(addedEmoji); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index f35a6667f4..fbc05dabd0 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -56,6 +56,10 @@ export const meta = { type: 'string', optional: false, nullable: false, }, + sortKey: { + type: 'string', + optional: false, nullable: true, + }, }, }, }, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 3caa0f84a3..957134c1ad 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -56,6 +56,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { type: 'string', } }, + sortKey: { type: 'string', nullable: true }, }, anyOf: [ { required: ['id'] }, @@ -104,6 +105,7 @@ export default class extends Endpoint { // eslint- isSensitive: ps.isSensitive, localOnly: ps.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, + sortKey: ps.sortKey, }, me); }); } diff --git a/packages/frontend-embed/vite.config.local-dev.ts b/packages/frontend-embed/vite.config.local-dev.ts index bf2f478887..528e28aa75 100644 --- a/packages/frontend-embed/vite.config.local-dev.ts +++ b/packages/frontend-embed/vite.config.local-dev.ts @@ -31,7 +31,7 @@ const devConfig: UserConfig = { publicDir: '../assets', base: '/embed', server: { - host: 'localhost', + host: true, port: 5174, proxy: { '/api': { diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts index b95533c2cd..785aefbf8b 100644 --- a/packages/frontend-embed/vite.config.ts +++ b/packages/frontend-embed/vite.config.ts @@ -64,6 +64,7 @@ export function getConfig(): UserConfig { server: { port: 5174, + host: true }, plugins: [ diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue index c7f1288729..d964061519 100644 --- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue +++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue @@ -49,6 +49,13 @@ SPDX-License-Identifier: AGPL-3.0-only {{ emoji.url }} + + + + diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index 151843b18c..b7f6811420 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-for="child in customEmojiTree" :key="`custom:${child.value}`" :initialShown="initialShown" - :emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))" + :emojis="computed(() => customEmojis.filter(e => e.category === child.category).sort(compareBySortKey).map(e => `:${e.name}:`))" :hasChildSection="child.children.length !== 0" :customEmojiTree="child.children" @chosen="nestedChosen" @@ -64,7 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, computed, Ref } from 'vue'; import { CustomEmojiFolderTree, getEmojiName } from '@@/js/emojilist.js'; import { i18n } from '@/i18n.js'; -import { customEmojis } from '@/custom-emojis.js'; +import { customEmojis, compareBySortKey } from '@/custom-emojis.js'; import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue'; const props = defineProps<{ diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 949ed4db91..99905081d3 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -91,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-for="child in customEmojiFolderRoot.children" :key="`custom:${child.value}`" :initialShown="false" - :emojis="computed(() => customEmojis.filter(e => filterCategory(e, child.value)).map(e => `:${e.name}:`))" + :emojis="computed(() => customEmojis.filter(e => filterCategory(e, child.value)).sort(compareBySortKey).map(e => `:${e.name}:`))" :disabledEmojis="computed(() => customEmojis.filter(e => filterCategory(e, child.value)).filter(e => !canReact(e)).map(e => `:${e.name}:`))" :hasChildSection="child.children.length !== 0" :customEmojiTree="child.children" @@ -133,7 +133,7 @@ import { isTouchUsing } from '@/scripts/touch.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; -import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js'; +import { customEmojiCategories, customEmojis, customEmojisMap, compareBySortKey } from '@/custom-emojis.js'; import { $i } from '@/account.js'; import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts index 0d03282cee..4c8ae46599 100644 --- a/packages/frontend/src/custom-emojis.ts +++ b/packages/frontend/src/custom-emojis.ts @@ -20,6 +20,19 @@ export const customEmojiCategories = computed<[ ...string[], null ]>(() => { return markRaw([...Array.from(categories), null]); }); +export function compareBySortKey(a: Misskey.entities.EmojiSimple, b: Misskey.entities.EmojiSimple): number { + if (a.sortKey === b.sortKey) { + if (a.name === b.name) + return 0; + return (a.name > b.name) ? 1 : -1; + } + if (a.sortKey === null) + return 1; + if (b.sortKey === null) + return -1; + return (a.sortKey > b.sortKey) ? 1 : -1; +} + export const customEmojisMap = new Map(); watch(customEmojis, emojis => { customEmojisMap.clear(); diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index c5f0dde878..b337e6900c 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -68,6 +68,9 @@ SPDX-License-Identifier: AGPL-3.0-only isSensitive {{ i18n.ts.localOnly }} + + + {{ i18n.ts.delete }} @@ -107,6 +110,7 @@ const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false); const localOnly = ref(props.emoji ? props.emoji.localOnly : false); const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); const rolesThatCanBeUsedThisEmojiAsReaction = ref([]); +const sortKey = ref(props.emoji ? (props.emoji.sortKey ?? '') : ''); const file = ref(); watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => { @@ -153,6 +157,7 @@ async function done() { isSensitive: isSensitive.value, localOnly: localOnly.value, roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id), + sortKey: sortKey.value === '' ? null : sortKey.value, }; if (file.value) { diff --git a/packages/frontend/vite.config.local-dev.ts b/packages/frontend/vite.config.local-dev.ts index d588f83138..b5fd797ec1 100644 --- a/packages/frontend/vite.config.local-dev.ts +++ b/packages/frontend/vite.config.local-dev.ts @@ -32,7 +32,7 @@ const devConfig: UserConfig = { publicDir: '../assets', base: './', server: { - host: 'localhost', + host: true, port: 5173, proxy: { '/api': { diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index f8bd433335..3333cd12fa 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -66,6 +66,7 @@ export function getConfig(): UserConfig { server: { port: 5173, + host: true, headers: { // なんか効かない 'X-Frame-Options': 'DENY', }, From cfa273adfa388f42b35064faaba580e6a2b47dbb Mon Sep 17 00:00:00 2001 From: HellhoundSoftware Date: Thu, 5 Dec 2024 23:59:40 -0500 Subject: [PATCH 2/5] Add language string --- locales/index.d.ts | 4 ++++ sharkey-locales/en-US.yml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/locales/index.d.ts b/locales/index.d.ts index d1cb1f97ea..71ddb57297 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -11374,6 +11374,10 @@ export interface Locale extends ILocale { * Remote followers may have incomplete or outdated activity */ "remoteFollowersWarning": string; + /** + * Sort key + */ + "sortKey": string; } declare const locales: { [lang: string]: Locale; diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 163fd0b0ae..290211d5f7 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -397,3 +397,5 @@ _auth: allowed: "Allowed" _announcement: new: "New" + +sortKey: "Sort key" From 38fec5f18f80df9ea1d75bae113f63ec6d680d8d Mon Sep 17 00:00:00 2001 From: HellhoundSoftware Date: Thu, 19 Dec 2024 18:48:46 -0500 Subject: [PATCH 3/5] Create CustomEmojiFolderTree nodes with correct path (fixes #25) --- packages/frontend/src/components/MkEmojiPicker.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 99905081d3..e413510164 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -186,11 +186,13 @@ function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): Cu const parts = input.split('/').map(p => p.trim()); let currentNode: CustomEmojiFolderTree = root; + let currentPath = []; for (const part of parts) { + currentPath.push(part); let existingNode = currentNode.children.find((node) => node.value === part); if (!existingNode) { - const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] }; + const newNode: CustomEmojiFolderTree = { value: part, category: currentPath.join("/"), children: [] }; currentNode.children.push(newNode); existingNode = newNode; } From 6a6c345698d4fdc141169ee0b053741f332dda45 Mon Sep 17 00:00:00 2001 From: HellhoundSoftware Date: Thu, 19 Dec 2024 21:15:01 -0500 Subject: [PATCH 4/5] Improve emoticon picker UI --- locales/index.d.ts | 38 +++++++++++++++++++ .../src/components/MkEmojiPicker.section.vue | 17 +++++---- .../frontend/src/components/MkEmojiPicker.vue | 24 ++++++++++-- sharkey-locales/en-US.yml | 11 ++++++ 4 files changed, 80 insertions(+), 10 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index 71ddb57297..4948bc1817 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -11378,6 +11378,44 @@ export interface Locale extends ILocale { * Sort key */ "sortKey": string; + "_unicodeEmoji": { + /** + * Smileys + */ + "face": string; + /** + * People + */ + "people": string; + /** + * Animals & nature + */ + "animals_and_nature": string; + /** + * Food & drink + */ + "food_and_drink": string; + /** + * Activity + */ + "activity": string; + /** + * Travel & places + */ + "travel_and_places": string; + /** + * Objects + */ + "objects": string; + /** + * Symbols + */ + "symbols": string; + /** + * Flags + */ + "flags": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index b7f6811420..b8a78e9e6a 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -6,9 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only