From 360e805638e108804933f2f8ebb47b680feeda76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=8B=E3=81=A1=E3=83=BC=E3=81=8B?= <7106976+EbiseLutica@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:29:17 +0900 Subject: [PATCH] =?UTF-8?q?enhance:=20=E3=82=A2=E3=83=90=E3=82=BF=E3=83=BC?= =?UTF-8?q?=E3=83=87=E3=82=B3=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=81=B8=E3=81=AE=E3=82=AB=E3=83=86=E3=82=B4=E3=83=AA=E3=81=AE?= =?UTF-8?q?=E5=B0=8E=E5=85=A5=20(#17034)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(backend): AvatarDecorationにcategoryを追加し、関連APIのプロパティ・戻り値にも反映 * feat(frontend): アバターデコレーションのカテゴリ設定機能 * chore(frontend): 管理画面とユーザー側の画面で、アバターデコレーションのグループ化のコードをある程度統一 * CHANGELOGを更新 * fix: group-avatar-decorations.tsを使用するよう修正 * chore: コーディング規約への準拠 * 型エラーを解消 --- CHANGELOG.md | 1 + ...3085-add-category-to-avatar-decorations.js | 22 ++++++++++++++ .../backend/src/models/AvatarDecoration.ts | 5 ++++ .../admin/avatar-decorations/create.ts | 7 +++++ .../admin/avatar-decorations/list.ts | 5 ++++ .../admin/avatar-decorations/update.ts | 2 ++ .../api/endpoints/get-avatar-decorations.ts | 5 ++++ .../pages/avatar-decoration-edit-dialog.vue | 6 ++++ .../frontend/src/pages/avatar-decorations.vue | 30 ++++++++++++------- .../src/pages/settings/avatar-decoration.vue | 23 ++++++++------ .../src/utility/group-avatar-decorations.ts | 25 ++++++++++++++++ packages/misskey-js/src/autogen/types.ts | 5 ++++ 12 files changed, 116 insertions(+), 20 deletions(-) create mode 100644 packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js create mode 100644 packages/frontend/src/utility/group-avatar-decorations.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index df57a3e862..f1da56245c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ - Enhance: 「もうすぐ誕生日のユーザー」ウィジェットで、誕生日が至近のユーザーも表示できるように (Cherry-picked from https://github.com/MisskeyIO/misskey) - 「今日誕生日のユーザー」は「もうすぐ誕生日のユーザー」に名称変更されました +- Enhance: アバターデコレーションにカテゴリを設定できるように - Fix: ユーザーハッシュタグページでユーザーの読み込みが重複する問題を修正 - 依存関係の更新 diff --git a/packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js b/packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js new file mode 100644 index 0000000000..a3410aa88e --- /dev/null +++ b/packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddCategoryToAvatarDecorations1766652173085 { + name = 'AddCategoryToAvatarDecorations1766652173085'; + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query('ALTER TABLE "avatar_decoration" ADD "category" character varying(128)'); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query('ALTER TABLE "avatar_decoration" DROP COLUMN "category"'); + } +}; diff --git a/packages/backend/src/models/AvatarDecoration.ts b/packages/backend/src/models/AvatarDecoration.ts index 13f0b05667..d14a210392 100644 --- a/packages/backend/src/models/AvatarDecoration.ts +++ b/packages/backend/src/models/AvatarDecoration.ts @@ -36,4 +36,9 @@ export class MiAvatarDecoration { array: true, length: 128, default: '{}', }) public roleIdsThatCanBeUsedThisDecoration: string[]; + + @Column('varchar', { + length: 128, nullable: true, + }) + public category: string | null; } diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts index 0121c302ac..baa87dbbbe 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts @@ -55,6 +55,10 @@ export const meta = { format: 'id', }, }, + category: { + type: 'string', + optional: false, nullable: true, + }, }, }, } as const; @@ -68,6 +72,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: { type: 'string', } }, + category: { type: 'string', nullable: true }, }, required: ['name', 'description', 'url'], } as const; @@ -84,6 +89,7 @@ export default class extends Endpoint { // eslint- description: ps.description, url: ps.url, roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration, + category: ps.category, }, me); return { @@ -94,6 +100,7 @@ export default class extends Endpoint { // eslint- description: created.description, url: created.url, roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration, + category: created.category, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts index 765bfd6766..7be3d79fee 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts @@ -60,6 +60,10 @@ export const meta = { format: 'id', }, }, + category: { + type: 'string', + optional: true, nullable: true, + }, }, }, }, @@ -95,6 +99,7 @@ export default class extends Endpoint { // eslint- description: avatarDecoration.description, url: avatarDecoration.url, roleIdsThatCanBeUsedThisDecoration: avatarDecoration.roleIdsThatCanBeUsedThisDecoration, + category: avatarDecoration.category, })); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts index 22476a6888..b84b4c5085 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts @@ -30,6 +30,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: { type: 'string', } }, + category: { type: 'string', nullable: true }, }, required: ['id'], } as const; @@ -45,6 +46,7 @@ export default class extends Endpoint { // eslint- description: ps.description, url: ps.url, roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration, + category: ps.category, }, me); }); } diff --git a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts index 52acee1cfb..ca0a5e2e25 100644 --- a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts +++ b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts @@ -49,6 +49,10 @@ export const meta = { format: 'id', }, }, + category: { + type: 'string', + optional: true, nullable: true, + }, }, }, }, @@ -76,6 +80,7 @@ export default class extends Endpoint { // eslint- description: decoration.description, url: decoration.url, roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(role => role.id === roleId)), + category: decoration.category, })); }); } diff --git a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue index 68e8d6a4d0..b1cb6f2f86 100644 --- a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue +++ b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue @@ -32,6 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only + + + @@ -79,6 +82,7 @@ const $i = ensureSignin(); const props = defineProps<{ avatarDecoration?: Misskey.entities.AdminAvatarDecorationsListResponse[number], + categories?: string[], }>(); const emit = defineEmits<{ @@ -89,6 +93,7 @@ const emit = defineEmits<{ const windowEl = useTemplateRef('windowEl'); const url = ref(props.avatarDecoration ? props.avatarDecoration.url : ''); const name = ref(props.avatarDecoration ? props.avatarDecoration.name : ''); +const category = ref(props.avatarDecoration?.category ? props.avatarDecoration.category : ''); const description = ref(props.avatarDecoration ? props.avatarDecoration.description : ''); const roleIdsThatCanBeUsedThisDecoration = ref(props.avatarDecoration ? props.avatarDecoration.roleIdsThatCanBeUsedThisDecoration : []); const rolesThatCanBeUsedThisDecoration = ref([]); @@ -118,6 +123,7 @@ async function done() { url: url.value, name: name.value, description: description.value, + category: category.value, roleIdsThatCanBeUsedThisDecoration: rolesThatCanBeUsedThisDecoration.value.map(x => x.id), }; diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue index 4c5457504e..b220f64978 100644 --- a/packages/frontend/src/pages/avatar-decorations.vue +++ b/packages/frontend/src/pages/avatar-decorations.vue @@ -7,18 +7,21 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
-
{{ avatarDecoration.name }}
- + + +
+
+
{{ avatarDecoration.name }}
+ +
-
+
@@ -32,10 +35,13 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; +import { groupAvatarDecorations } from '@/utility/group-avatar-decorations.js'; const $i = ensureSignin(); const avatarDecorations = ref([]); +const groupedDecorations = computed(() => groupAvatarDecorations(avatarDecorations.value)); function load() { misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => { @@ -47,6 +53,7 @@ load(); async function add(ev: PointerEvent) { const { dispose } = await os.popupAsyncWithDialog(import('./avatar-decoration-edit-dialog.vue').then(x => x.default), { + categories: Object.keys(groupedDecorations.value), }, { done: result => { if (result.created) { @@ -60,6 +67,7 @@ async function add(ev: PointerEvent) { async function edit(avatarDecoration: Misskey.entities.AdminAvatarDecorationsListResponse[number]) { const { dispose } = await os.popupAsyncWithDialog(import('./avatar-decoration-edit-dialog.vue').then(x => x.default), { avatarDecoration: avatarDecoration, + categories: Object.keys(groupedDecorations.value), }, { done: result => { if (result.updated) { diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue index d3d642f156..34fb891806 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.vue @@ -29,15 +29,17 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.detachAll }}
- -
- -
+ + +
+ +
+
@@ -52,17 +54,20 @@ import * as Misskey from 'misskey-js'; import XDecoration from './avatar-decoration.decoration.vue'; import XDialog from './avatar-decoration.dialog.vue'; import MkButton from '@/components/MkButton.vue'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import { ensureSignin } from '@/i.js'; import MkInfo from '@/components/MkInfo.vue'; import { definePage } from '@/page.js'; +import { groupAvatarDecorations } from '@/utility/group-avatar-decorations.js'; const $i = ensureSignin(); const loading = ref(true); const avatarDecorations = ref([]); +const groupedDecorations = computed(() => groupAvatarDecorations(avatarDecorations.value)); misskeyApi('get-avatar-decorations').then(_avatarDecorations => { avatarDecorations.value = _avatarDecorations; diff --git a/packages/frontend/src/utility/group-avatar-decorations.ts b/packages/frontend/src/utility/group-avatar-decorations.ts new file mode 100644 index 0000000000..285913bcfd --- /dev/null +++ b/packages/frontend/src/utility/group-avatar-decorations.ts @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +type AvatarDecorationBase = { category?: string | null | undefined }; + +/** + * アバターデコレーションをカテゴリごとにグループ化します。 + * @param decorations アバターデコレーションの配列 + * @returns カテゴリごとにグループ化されたアバターデコレーションオブジェクト + */ +export function groupAvatarDecorations(decorations: T[]) { + const grouped: Record = {}; + + for (const decoration of decorations) { + const category = decoration.category ?? ''; + if (!(category in grouped)) { + grouped[category] = []; + } + grouped[category].push(decoration); + } + + return grouped; +} diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 46d04ac2dc..df08b3f804 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -6964,6 +6964,7 @@ export interface operations { description: string; url: string; roleIdsThatCanBeUsedThisDecoration?: string[]; + category?: string | null; }; }; }; @@ -6985,6 +6986,7 @@ export interface operations { description: string; url: string; roleIdsThatCanBeUsedThisDecoration: string[]; + category: string | null; }; }; }; @@ -7136,6 +7138,7 @@ export interface operations { description: string; url: string; roleIdsThatCanBeUsedThisDecoration: string[]; + category?: string | null; }[]; }; }; @@ -7196,6 +7199,7 @@ export interface operations { description?: string; url?: string; roleIdsThatCanBeUsedThisDecoration?: string[]; + category?: string | null; }; }; }; @@ -23633,6 +23637,7 @@ export interface operations { description: string; url: string; roleIdsThatCanBeUsedThisDecoration: string[]; + category?: string | null; }[]; }; };