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', },