From a45ccc18b403682be3f1af901aec5a542401ea08 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:33:41 +0900 Subject: [PATCH 01/21] refactor --- packages/backend/test/unit/ReactionService.ts | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/packages/backend/test/unit/ReactionService.ts b/packages/backend/test/unit/ReactionService.ts index 1957f4544c..3cfb4ff3f8 100644 --- a/packages/backend/test/unit/ReactionService.ts +++ b/packages/backend/test/unit/ReactionService.ts @@ -21,73 +21,73 @@ describe('ReactionService', () => { }); describe('normalize', () => { - test('絵文字リアクションはそのまま', async () => { - assert.strictEqual(await reactionService.normalize('👍'), '👍'); - assert.strictEqual(await reactionService.normalize('🍅'), '🍅'); + test('絵文字リアクションはそのまま', () => { + assert.strictEqual(reactionService.normalize('👍'), '👍'); + assert.strictEqual(reactionService.normalize('🍅'), '🍅'); }); - test('既存のリアクションは絵文字化する pudding', async () => { - assert.strictEqual(await reactionService.normalize('pudding'), '🍮'); + test('既存のリアクションは絵文字化する pudding', () => { + assert.strictEqual(reactionService.normalize('pudding'), '🍮'); }); - test('既存のリアクションは絵文字化する like', async () => { - assert.strictEqual(await reactionService.normalize('like'), '👍'); + test('既存のリアクションは絵文字化する like', () => { + assert.strictEqual(reactionService.normalize('like'), '👍'); }); - test('既存のリアクションは絵文字化する love', async () => { - assert.strictEqual(await reactionService.normalize('love'), '❤'); + test('既存のリアクションは絵文字化する love', () => { + assert.strictEqual(reactionService.normalize('love'), '❤'); }); - test('既存のリアクションは絵文字化する laugh', async () => { - assert.strictEqual(await reactionService.normalize('laugh'), '😆'); + test('既存のリアクションは絵文字化する laugh', () => { + assert.strictEqual(reactionService.normalize('laugh'), '😆'); }); - test('既存のリアクションは絵文字化する hmm', async () => { - assert.strictEqual(await reactionService.normalize('hmm'), '🤔'); + test('既存のリアクションは絵文字化する hmm', () => { + assert.strictEqual(reactionService.normalize('hmm'), '🤔'); }); - test('既存のリアクションは絵文字化する surprise', async () => { - assert.strictEqual(await reactionService.normalize('surprise'), '😮'); + test('既存のリアクションは絵文字化する surprise', () => { + assert.strictEqual(reactionService.normalize('surprise'), '😮'); }); - test('既存のリアクションは絵文字化する congrats', async () => { - assert.strictEqual(await reactionService.normalize('congrats'), '🎉'); + test('既存のリアクションは絵文字化する congrats', () => { + assert.strictEqual(reactionService.normalize('congrats'), '🎉'); }); - test('既存のリアクションは絵文字化する angry', async () => { - assert.strictEqual(await reactionService.normalize('angry'), '💢'); + test('既存のリアクションは絵文字化する angry', () => { + assert.strictEqual(reactionService.normalize('angry'), '💢'); }); - test('既存のリアクションは絵文字化する confused', async () => { - assert.strictEqual(await reactionService.normalize('confused'), '😥'); + test('既存のリアクションは絵文字化する confused', () => { + assert.strictEqual(reactionService.normalize('confused'), '😥'); }); - test('既存のリアクションは絵文字化する rip', async () => { - assert.strictEqual(await reactionService.normalize('rip'), '😇'); + test('既存のリアクションは絵文字化する rip', () => { + assert.strictEqual(reactionService.normalize('rip'), '😇'); }); - test('既存のリアクションは絵文字化する star', async () => { - assert.strictEqual(await reactionService.normalize('star'), '⭐'); + test('既存のリアクションは絵文字化する star', () => { + assert.strictEqual(reactionService.normalize('star'), '⭐'); }); - test('異体字セレクタ除去', async () => { - assert.strictEqual(await reactionService.normalize('㊗️'), '㊗'); + test('異体字セレクタ除去', () => { + assert.strictEqual(reactionService.normalize('㊗️'), '㊗'); }); - test('異体字セレクタ除去 必要なし', async () => { - assert.strictEqual(await reactionService.normalize('㊗'), '㊗'); + test('異体字セレクタ除去 必要なし', () => { + assert.strictEqual(reactionService.normalize('㊗'), '㊗'); }); - test('fallback - null', async () => { - assert.strictEqual(await reactionService.normalize(null), '❤'); + test('fallback - null', () => { + assert.strictEqual(reactionService.normalize(null), '❤'); }); - test('fallback - empty', async () => { - assert.strictEqual(await reactionService.normalize(''), '❤'); + test('fallback - empty', () => { + assert.strictEqual(reactionService.normalize(''), '❤'); }); - test('fallback - unknown', async () => { - assert.strictEqual(await reactionService.normalize('unknown'), '❤'); + test('fallback - unknown', () => { + assert.strictEqual(reactionService.normalize('unknown'), '❤'); }); }); From bf17092b41164e293ab8f2912a3a80a6d316bf0f Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 5 Jul 2025 08:18:15 +0900 Subject: [PATCH 02/21] =?UTF-8?q?test:=20VS=20Code=E4=B8=8A=E3=81=A7?= =?UTF-8?q?=E8=A4=87=E6=95=B0=E3=81=AEjest=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=20(#16251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 0ceec23acd..5f36a32af4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,8 +6,12 @@ "files.associations": { "*.test.ts": "typescript" }, - "jest.jestCommandLine": "pnpm run jest", "jest.runMode": "on-demand", + "jest.virtualFolders": [ + { "name": "backend unit", "jestCommandLine": "pnpm -F backend run test" }, + { "name": "backend e2e", "jestCommandLine": "pnpm -F backend run test:e2e"}, + { "name": "misskey-js", "jestCommandLine": "pnpm -F misskey-js run jest" } + ], "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, From 7cf1eccd0434229b1b5d261d44e5e0bccf7a81ee Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 5 Jul 2025 08:31:20 +0900 Subject: [PATCH 03/21] clean up --- packages/frontend/src/pages/user/home.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index ea77444afd..ed3ae6a2aa 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -25,7 +25,6 @@ SPDX-License-Identifier: AGPL-3.0-only
-
@@ -23,7 +21,6 @@ SPDX-License-Identifier: AGPL-3.0-only import { markRaw } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import MkNote from '@/components/MkNote.vue'; -import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { Paginator } from '@/utility/paginator.js'; From e6ec15e397e150a12486d097d4b789a98b7ae639 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 6 Jul 2025 09:54:49 +0900 Subject: [PATCH 12/21] =?UTF-8?q?feat:=20=E7=89=B9=E5=AE=9A=E3=81=AE?= =?UTF-8?q?=E3=83=89=E3=83=A9=E3=82=A4=E3=83=96=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E6=B7=BB=E4=BB=98=E3=81=97=E3=81=A6=E3=81=84?= =?UTF-8?q?=E3=82=8B=E3=83=81=E3=83=A3=E3=83=83=E3=83=88=E3=83=A1=E3=83=83?= =?UTF-8?q?=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92=E4=B8=80=E8=A6=A7=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + locales/index.d.ts | 4 + locales/ja-JP.yml | 1 + .../backend/src/server/api/endpoint-list.ts | 1 + .../drive/files/attached-chat-messages.ts | 85 +++++++++++++++++++ .../frontend/src/components/MkUserList.vue | 2 +- .../frontend/src/pages/admin-file.chat.vue | 38 +++++++++ packages/frontend/src/pages/admin-file.vue | 24 ++++-- packages/misskey-js/etc/misskey-js.api.md | 8 ++ .../misskey-js/src/autogen/apiClientJSDoc.ts | 11 +++ packages/misskey-js/src/autogen/endpoint.ts | 3 + packages/misskey-js/src/autogen/entities.ts | 2 + packages/misskey-js/src/autogen/types.ts | 83 ++++++++++++++++++ 13 files changed, 257 insertions(+), 6 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/drive/files/attached-chat-messages.ts create mode 100644 packages/frontend/src/pages/admin-file.chat.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 516f5f5fcd..cf422f5808 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Feat: ノートの下書き機能 - Feat: クリップ内でノートを検索できるように - Feat: Playを検索できるように +- Feat: モデレーションにおいて、特定のドライブファイルを添付しているチャットメッセージを一覧できるように ### Client - Feat: モデログを検索できるように diff --git a/locales/index.d.ts b/locales/index.d.ts index ce6ee44653..d5ec9f5d77 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -10890,6 +10890,10 @@ export interface Locale extends ILocale { * 添付されているノート */ "attachedNotes": string; + /** + * 利用 + */ + "usage": string; /** * このページは、このファイルをアップロードしたユーザーしか閲覧できません。 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0d2d8c4611..760f89ef64 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2885,6 +2885,7 @@ _fileViewer: url: "URL" uploadedAt: "追加日" attachedNotes: "添付されているノート" + usage: "利用" thisPageCanBeSeenFromTheAuthor: "このページは、このファイルをアップロードしたユーザーしか閲覧できません。" _externalResourceInstaller: diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index eb83c11b39..5c4a58a6fc 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -168,6 +168,7 @@ export * as 'clips/update' from './endpoints/clips/update.js'; export * as 'drive' from './endpoints/drive.js'; export * as 'drive/files' from './endpoints/drive/files.js'; export * as 'drive/files/attached-notes' from './endpoints/drive/files/attached-notes.js'; +export * as 'drive/files/attached-chat-messages' from './endpoints/drive/files/attached-chat-messages.js'; export * as 'drive/files/check-existence' from './endpoints/drive/files/check-existence.js'; export * as 'drive/files/create' from './endpoints/drive/files/create.js'; export * as 'drive/files/delete' from './endpoints/drive/files/delete.js'; diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-chat-messages.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-chat-messages.ts new file mode 100644 index 0000000000..5be477f468 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-chat-messages.ts @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { DriveFilesRepository, ChatMessagesRepository } from '@/models/_.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['drive', 'chat'], + + requireCredential: true, + + kind: 'read:drive', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatMessage', + }, + }, + + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: '485ce26d-f5d2-4313-9783-e689d131eafb', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + fileId: { type: 'string', format: 'misskey:id' }, + }, + required: ['fileId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.chatMessagesRepository) + private chatMessagesRepository: ChatMessagesRepository, + + private chatEntityService: ChatEntityService, + private queryService: QueryService, + private roleService: RoleService, + ) { + super(meta, paramDef, async (ps, me) => { + const file = await this.driveFilesRepository.findOneBy({ + id: ps.fileId, + userId: await this.roleService.isModerator(me) ? undefined : me.id, + }); + + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } + + const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate); + query.andWhere('message.fileId = :fileId', { fileId: file.id }); + + const messages = await query.limit(ps.limit).getMany(); + + return await this.chatEntityService.packMessagesDetailed(messages, me); + }); + } +} diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue index c639e18b1d..e3469d0fd7 100644 --- a/packages/frontend/src/components/MkUserList.vue +++ b/packages/frontend/src/components/MkUserList.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/admin-file.chat.vue b/packages/frontend/src/pages/admin-file.chat.vue new file mode 100644 index 0000000000..e451da51a3 --- /dev/null +++ b/packages/frontend/src/pages/admin-file.chat.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue index 8495642a8c..7a49ba542f 100644 --- a/packages/frontend/src/pages/admin-file.vue +++ b/packages/frontend/src/pages/admin-file.vue @@ -44,8 +44,19 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.delete }} -
- +
+ + +
{{ i18n.ts.requireAdminForView }} @@ -86,12 +97,15 @@ import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { iAmAdmin, iAmModerator } from '@/i.js'; +import MkTabs from '@/components/MkTabs.vue'; const tab = ref('overview'); const file = ref(null); const info = ref(null); const isSensitive = ref(false); +const usageTab = ref<'note' | 'chat'>('note'); const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue')); +const XChat = defineAsyncComponent(() => import('./admin-file.chat.vue')); const props = defineProps<{ fileId: string, @@ -147,9 +161,9 @@ const headerTabs = computed(() => [{ title: i18n.ts.overview, icon: 'ti ti-info-circle', }, iAmModerator ? { - key: 'notes', - title: i18n.ts._fileViewer.attachedNotes, - icon: 'ti ti-pencil', + key: 'usage', + title: i18n.ts._fileViewer.usage, + icon: 'ti ti-plus', } : null, iAmModerator ? { key: 'ip', title: 'IP', diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index f38e959fb2..d584efe731 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1226,6 +1226,12 @@ type DateString = string; // @public (undocumented) type DriveFile = components['schemas']['DriveFile']; +// @public (undocumented) +type DriveFilesAttachedChatMessagesRequest = operations['drive___files___attached-chat-messages']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DriveFilesAttachedChatMessagesResponse = operations['drive___files___attached-chat-messages']['responses']['200']['content']['application/json']; + // @public (undocumented) type DriveFilesAttachedNotesRequest = operations['drive___files___attached-notes']['requestBody']['content']['application/json']; @@ -1740,6 +1746,8 @@ declare namespace entities { DriveResponse, DriveFilesRequest, DriveFilesResponse, + DriveFilesAttachedChatMessagesRequest, + DriveFilesAttachedChatMessagesResponse, DriveFilesAttachedNotesRequest, DriveFilesAttachedNotesResponse, DriveFilesCheckExistenceRequest, diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 60e238351c..4a13045592 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -2018,6 +2018,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * Find the notes to which the given file is attached. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 929cca183f..5ef493946c 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -275,6 +275,8 @@ import type { DriveResponse, DriveFilesRequest, DriveFilesResponse, + DriveFilesAttachedChatMessagesRequest, + DriveFilesAttachedChatMessagesResponse, DriveFilesAttachedNotesRequest, DriveFilesAttachedNotesResponse, DriveFilesCheckExistenceRequest, @@ -833,6 +835,7 @@ export type Endpoints = { 'clips/update': { req: ClipsUpdateRequest; res: ClipsUpdateResponse }; 'drive': { req: EmptyRequest; res: DriveResponse }; 'drive/files': { req: DriveFilesRequest; res: DriveFilesResponse }; + 'drive/files/attached-chat-messages': { req: DriveFilesAttachedChatMessagesRequest; res: DriveFilesAttachedChatMessagesResponse }; 'drive/files/attached-notes': { req: DriveFilesAttachedNotesRequest; res: DriveFilesAttachedNotesResponse }; 'drive/files/check-existence': { req: DriveFilesCheckExistenceRequest; res: DriveFilesCheckExistenceResponse }; 'drive/files/create': { req: DriveFilesCreateRequest; res: DriveFilesCreateResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 002dfaaf30..a11bbefde5 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -278,6 +278,8 @@ export type ClipsUpdateResponse = operations['clips___update']['responses']['200 export type DriveResponse = operations['drive']['responses']['200']['content']['application/json']; export type DriveFilesRequest = operations['drive___files']['requestBody']['content']['application/json']; export type DriveFilesResponse = operations['drive___files']['responses']['200']['content']['application/json']; +export type DriveFilesAttachedChatMessagesRequest = operations['drive___files___attached-chat-messages']['requestBody']['content']['application/json']; +export type DriveFilesAttachedChatMessagesResponse = operations['drive___files___attached-chat-messages']['responses']['200']['content']['application/json']; export type DriveFilesAttachedNotesRequest = operations['drive___files___attached-notes']['requestBody']['content']['application/json']; export type DriveFilesAttachedNotesResponse = operations['drive___files___attached-notes']['responses']['200']['content']['application/json']; export type DriveFilesCheckExistenceRequest = operations['drive___files___check-existence']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index bea4090aba..df6a22ec41 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -1653,6 +1653,15 @@ export type paths = { */ post: operations['drive___files']; }; + '/drive/files/attached-chat-messages': { + /** + * drive/files/attached-chat-messages + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:drive* + */ + post: operations['drive___files___attached-chat-messages']; + }; '/drive/files/attached-notes': { /** * drive/files/attached-notes @@ -18748,6 +18757,80 @@ export interface operations { }; }; }; + 'drive___files___attached-chat-messages': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + fileId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ChatMessage'][]; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; 'drive___files___attached-notes': { requestBody: { content: { From 40a35968f0a1e49a6ccae43028790b01c7e39ec6 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 6 Jul 2025 15:54:33 +0900 Subject: [PATCH 13/21] clean up --- packages/frontend/src/style.scss | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 6a9aa08b30..7027b844d8 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -45,27 +45,6 @@ html { &, * { scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent; scrollbar-width: thin; - - &::-webkit-scrollbar { - width: 6px; - height: 6px; - } - - &::-webkit-scrollbar-track { - background: inherit; - } - - &::-webkit-scrollbar-thumb { - background: var(--MI_THEME-scrollbarHandle); - - &:hover { - background: var(--MI_THEME-scrollbarHandleHover); - } - - &:active { - background: var(--MI_THEME-accent); - } - } } &.f-1 { From 004cfd5e4b3847175653cafa225fe7a6480af728 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 6 Jul 2025 15:57:21 +0900 Subject: [PATCH 14/21] clean up --- packages/frontend/src/components/MkEmojiPicker.vue | 5 ----- packages/frontend/src/components/MkTabs.vue | 4 ---- .../src/components/global/MkPageHeader.tabs.vue | 4 ---- packages/frontend/src/ui/deck/column.vue | 12 ------------ 4 files changed, 25 deletions(-) diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 86f019c95c..68da098439 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -684,13 +684,8 @@ defineExpose({ height: 100%; overflow-y: auto; overflow-x: hidden; - scrollbar-width: none; - &::-webkit-scrollbar { - display: none; - } - > .group { &:not(.index) { padding: 4px 0 8px 0; diff --git a/packages/frontend/src/components/MkTabs.vue b/packages/frontend/src/components/MkTabs.vue index a1f30100d0..5c4a67b026 100644 --- a/packages/frontend/src/components/MkTabs.vue +++ b/packages/frontend/src/components/MkTabs.vue @@ -169,10 +169,6 @@ onUnmounted(() => { overflow-x: auto; overflow-y: hidden; scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } } .tabsInner { diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue index 255fca8f86..f2173b2e22 100644 --- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -194,10 +194,6 @@ onUnmounted(() => { overflow-x: auto; overflow-y: hidden; scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } } .tabsInner { diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index 4e79b301e3..11937fda24 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -368,10 +368,6 @@ function onDrop(ev) { > .body { background: transparent !important; scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent; - - &::-webkit-scrollbar-track { - background: transparent; - } } } @@ -397,10 +393,6 @@ function onDrop(ev) { > .body { background: var(--MI_THEME-bg) !important; scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent; - - &::-webkit-scrollbar-track { - background: inherit; - } } } } @@ -487,9 +479,5 @@ function onDrop(ev) { container-type: size; background-color: var(--MI_THEME-bg); scrollbar-color: var(--MI_THEME-scrollbarHandle) var(--MI_THEME-panel); - - &::-webkit-scrollbar-track { - background: var(--MI_THEME-panel); - } } From 9dddc847505501c9d269d6f3a8090203711cb0f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sun, 6 Jul 2025 17:24:34 +0900 Subject: [PATCH 15/21] =?UTF-8?q?refactor(frontend):=20menu=E3=81=AE?= =?UTF-8?q?=E5=9E=8B=E5=AE=9A=E7=BE=A9=E3=81=AE=E5=8F=AF=E8=AA=AD=E6=80=A7?= =?UTF-8?q?=E5=90=91=E4=B8=8A=20(#16261)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/types/menu.ts | 117 ++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 16 deletions(-) diff --git a/packages/frontend/src/types/menu.ts b/packages/frontend/src/types/menu.ts index fae7370341..6f2f08cb6d 100644 --- a/packages/frontend/src/types/menu.ts +++ b/packages/frontend/src/types/menu.ts @@ -4,10 +4,10 @@ */ import * as Misskey from 'misskey-js'; -import type { Component, ComputedRef, Ref } from 'vue'; +import type { Component, ComputedRef, Ref, MaybeRef } from 'vue'; import type { ComponentProps as CP } from 'vue-component-type-helpers'; -type ComponentProps = { [K in keyof CP]: CP[K] | Ref[K]> }; +type ComponentProps = { [K in keyof CP]: MaybeRef[K]> }; type MenuRadioOptionsDef = Record; @@ -15,22 +15,107 @@ type Text = string | ComputedRef; export type MenuAction = (ev: MouseEvent) => void; -export type MenuDivider = { type: 'divider' }; -export type MenuNull = undefined; -export type MenuLabel = { type: 'label', text: Text, caption?: Text }; -export type MenuLink = { type: 'link', to: string, text: Text, caption?: Text, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; -export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: Text, caption?: Text, icon?: string, indicate?: boolean }; -export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction }; -export type MenuSwitch = { type: 'switch', ref: Ref, text: Text, caption?: Text, icon?: string, disabled?: boolean | Ref }; -export type MenuButton = { type?: 'button', text: Text, caption?: Text, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef, avatar?: Misskey.entities.User; action: MenuAction }; -export type MenuRadio = { type: 'radio', text: Text, caption?: Text, icon?: string, ref: Ref, options: MenuRadioOptionsDef, disabled?: boolean | Ref }; -export type MenuRadioOption = { type: 'radioOption', text: Text, caption?: Text, action: MenuAction; active?: boolean | ComputedRef }; -export type MenuComponent = { type: 'component', component: T, props?: ComponentProps }; -export type MenuParent = { type: 'parent', text: Text, caption?: Text, icon?: string, children: MenuItem[] | (() => Promise | MenuItem[]) }; +export interface MenuButton { + type?: 'button'; + text: Text; + caption?: Text; + icon?: string; + indicate?: boolean; + danger?: boolean; + active?: boolean | ComputedRef; + avatar?: Misskey.entities.User; + action: MenuAction; +} -export type MenuPending = { type: 'pending' }; +interface MenuBase { + type: string; +} -type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuComponent | MenuParent; +export interface MenuDivider extends MenuBase { + type: 'divider'; +} + +export interface MenuLabel extends MenuBase { + type: 'label'; + text: Text; + caption?: Text; +} + +export interface MenuLink extends MenuBase { + type: 'link'; + to: string; + text: Text; + caption?: Text; + icon?: string; + indicate?: boolean; + avatar?: Misskey.entities.User; +} + +export interface MenuA extends MenuBase { + type: 'a'; + href: string; + target?: string; + download?: string; + text: Text; + caption?: Text; + icon?: string; + indicate?: boolean; +} + +export interface MenuUser extends MenuBase { + type: 'user'; + user: Misskey.entities.User; + active?: boolean; + indicate?: boolean; + action: MenuAction; +} + +export interface MenuSwitch extends MenuBase { + type: 'switch'; + ref: Ref; + text: Text; + caption?: Text; + icon?: string; + disabled?: boolean | Ref; +} + +export interface MenuRadio extends MenuBase { + type: 'radio'; + text: Text; + caption?: Text; + icon?: string; + ref: Ref; + options: MenuRadioOptionsDef; + disabled?: boolean | Ref; +} + +export interface MenuRadioOption extends MenuBase { + type: 'radioOption'; + text: Text; + caption?: Text; + action: MenuAction; + active?: boolean | ComputedRef; +} + +export interface MenuComponent extends MenuBase { + type: 'component'; + component: T; + props?: ComponentProps; +} + +export interface MenuParent extends MenuBase { + type: 'parent'; + text: Text; + caption?: Text; + icon?: string; + children: MenuItem[] | (() => Promise | MenuItem[]); +} + +export interface MenuPending extends MenuBase { + type: 'pending'; +} + +type OuterMenuItem = MenuDivider | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuComponent | MenuParent; type OuterPromiseMenuItem = Promise; export type MenuItem = OuterMenuItem | OuterPromiseMenuItem; export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuComponent | MenuParent; From 553ccff77c79c57172b34b2d0b2988740796c85b Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 6 Jul 2025 19:32:31 +0900 Subject: [PATCH 16/21] chore(frontend): tweak selector to improve rendering performance --- packages/frontend/src/style.scss | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 7027b844d8..ebd2b7e48c 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -22,11 +22,6 @@ } } -::selection { - color: var(--MI_THEME-fgOnAccent); - background-color: var(--MI_THEME-accent); -} - html { overflow: auto; overflow-wrap: break-word; @@ -89,6 +84,11 @@ html::view-transition-old(theme-changing) { animation-fill-mode: forwards; } +html::selection { + color: var(--MI_THEME-fgOnAccent); + background-color: var(--MI_THEME-accent); +} + @keyframes themeChangingOld { 0% { opacity: 1; From a8abb03d1785791ab40e57ab49c87640914532c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sun, 6 Jul 2025 19:36:11 +0900 Subject: [PATCH 17/21] =?UTF-8?q?refactor(frontend):=20Form=E3=81=BE?= =?UTF-8?q?=E3=82=8F=E3=82=8A=E3=81=AE=E5=9E=8B=E5=BC=B7=E5=8C=96=20(#1626?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(frontend): Formまわりの型強化 * fix * avoid non-null assertion and add null check for safety * refactor * avoid non-null assertion and add null check for safety * Update clip.vue --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- packages/frontend/src/os.ts | 4 +- packages/frontend/src/pages/clip.vue | 8 +- .../frontend/src/pages/my-clips/index.vue | 1 + packages/frontend/src/pages/registry.keys.vue | 2 + packages/frontend/src/pages/registry.vue | 2 + packages/frontend/src/plugin.ts | 5 +- packages/frontend/src/store.ts | 2 +- .../src/ui/deck/tl-note-notification.ts | 14 +- packages/frontend/src/utility/form.ts | 122 ++++++++++++------ .../frontend/src/utility/get-note-menu.ts | 2 +- .../frontend/src/utility/get-user-menu.ts | 2 + .../frontend/src/widgets/WidgetActivity.vue | 14 +- .../frontend/src/widgets/WidgetAichan.vue | 8 +- .../frontend/src/widgets/WidgetAiscript.vue | 10 +- .../src/widgets/WidgetAiscriptApp.vue | 10 +- .../src/widgets/WidgetBirthdayFollowings.vue | 8 +- .../frontend/src/widgets/WidgetButton.vue | 12 +- .../frontend/src/widgets/WidgetCalendar.vue | 6 +- packages/frontend/src/widgets/WidgetChat.vue | 6 +- .../frontend/src/widgets/WidgetClicker.vue | 6 +- packages/frontend/src/widgets/WidgetClock.vue | 70 ++++++---- .../src/widgets/WidgetDigitalClock.vue | 14 +- .../frontend/src/widgets/WidgetFederation.vue | 8 +- .../src/widgets/WidgetInstanceCloud.vue | 6 +- .../src/widgets/WidgetInstanceInfo.vue | 6 +- .../frontend/src/widgets/WidgetJobQueue.vue | 8 +- packages/frontend/src/widgets/WidgetMemo.vue | 10 +- .../src/widgets/WidgetNotifications.vue | 13 +- .../src/widgets/WidgetOnlineUsers.vue | 8 +- .../frontend/src/widgets/WidgetPhotos.vue | 8 +- .../frontend/src/widgets/WidgetPostForm.vue | 4 +- .../frontend/src/widgets/WidgetProfile.vue | 10 +- packages/frontend/src/widgets/WidgetRss.vue | 12 +- .../frontend/src/widgets/WidgetRssTicker.vue | 20 +-- .../frontend/src/widgets/WidgetSlideshow.vue | 19 +-- .../frontend/src/widgets/WidgetTimeline.vue | 50 ++++--- .../frontend/src/widgets/WidgetTrends.vue | 6 +- .../frontend/src/widgets/WidgetUnixClock.vue | 19 +-- .../frontend/src/widgets/WidgetUserList.vue | 16 +-- .../src/widgets/server-metric/cpu-mem.vue | 2 +- .../src/widgets/server-metric/cpu.vue | 2 +- .../src/widgets/server-metric/index.vue | 12 +- .../src/widgets/server-metric/mem.vue | 2 +- .../src/widgets/server-metric/net.vue | 2 +- packages/frontend/src/widgets/widget.ts | 12 +- 45 files changed, 344 insertions(+), 239 deletions(-) diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 83ad0ebdf9..bf0e5e1b37 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -13,7 +13,7 @@ import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { Form, GetFormResultType } from '@/utility/form.js'; import type { MenuItem } from '@/types/menu.js'; import type { PostFormProps } from '@/types/post-form.js'; -import type { UploaderDialogFeatures } from '@/components/MkUploaderDialog.vue'; +import type { UploaderFeatures } from '@/composables/use-uploader.js'; import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue'; import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -837,7 +837,7 @@ export function launchUploader( options?: { folderId?: string | null; multiple?: boolean; - features?: UploaderDialogFeatures; + features?: UploaderFeatures; }, ): Promise { return new Promise(async (res, rej) => { diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 8843812544..8176fb519b 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -76,7 +76,8 @@ watch(() => props.clipId, async () => { clip.value = await misskeyApi('clips/show', { clipId: props.clipId, }); - favorited.value = clip.value.isFavorited; + + favorited.value = clip.value!.isFavorited ?? false; }, { immediate: true, }); @@ -108,6 +109,8 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ icon: 'ti ti-pencil', text: i18n.ts.edit, handler: async (): Promise => { + if (clip.value == null) return; + const { canceled, result } = await os.form(clip.value.name, { name: { type: 'string', @@ -128,6 +131,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ default: clip.value.isPublic, }, }); + if (canceled) return; os.apiWithDialog('clips/update', { @@ -178,6 +182,8 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ text: i18n.ts.delete, danger: true, handler: async (): Promise => { + if (clip.value == null) return; + const { canceled } = await os.confirm({ type: 'warning', text: i18n.tsx.deleteAreYouSure({ x: clip.value.name }), diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index 4c664a0951..f48dc5be4d 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -64,6 +64,7 @@ async function create() { default: false, }, }); + if (canceled) return; os.apiWithDialog('clips/create', result); diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue index 39575fe1f7..8eb2ab9fd0 100644 --- a/packages/frontend/src/pages/registry.keys.vue +++ b/packages/frontend/src/pages/registry.keys.vue @@ -79,7 +79,9 @@ async function createKey() { default: scope.value.join('/'), }, }); + if (canceled) return; + os.apiWithDialog('i/registry/set', { scope: result.scope.split('/'), key: result.key, diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue index 5e59082b50..3762dadd12 100644 --- a/packages/frontend/src/pages/registry.vue +++ b/packages/frontend/src/pages/registry.vue @@ -56,7 +56,9 @@ async function createKey() { label: i18n.ts._registry.scope, }, }); + if (canceled) return; + os.apiWithDialog('i/registry/set', { scope: result.scope.split('/'), key: result.key, diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index 6010180e68..d6007a27ed 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -14,12 +14,13 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import { prefer } from '@/preferences.js'; +import type { FormWithDefault } from '@/utility/form.js'; export type Plugin = { installId: string; name: string; active: boolean; - config?: Record; + config?: FormWithDefault; configData: Record; src: string | null; version: string; @@ -240,7 +241,7 @@ async function launchPlugin(id: Plugin['installId']): Promise { pluginLogs.value.set(plugin.installId, []); function systemLog(message: string, isError = false): void { - pluginLogs.value.get(plugin.installId)?.push({ + pluginLogs.value.get(plugin!.installId)?.push({ at: Date.now(), isSystem: true, message, diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 9afaf2c9b9..e9402cfb70 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -29,7 +29,7 @@ export const store = markRaw(new Pizzax('base', { }, memo: { where: 'account', - default: null, + default: null as string | null, }, reactionAcceptance: { where: 'account', diff --git a/packages/frontend/src/ui/deck/tl-note-notification.ts b/packages/frontend/src/ui/deck/tl-note-notification.ts index 728c0d0d29..e9b14e70ee 100644 --- a/packages/frontend/src/ui/deck/tl-note-notification.ts +++ b/packages/frontend/src/ui/deck/tl-note-notification.ts @@ -29,7 +29,8 @@ export async function soundSettingsButton(soundSetting: Ref): Promis label: i18n.ts.sound, default: soundSetting.value.type ?? 'none', enum: soundsTypes.map(f => ({ - value: f ?? 'none', label: getSoundTypeName(f), + value: f ?? 'none' as Exclude | 'none', + label: getSoundTypeName(f), })), }, soundFile: { @@ -81,16 +82,17 @@ export async function soundSettingsButton(soundSetting: Ref): Promis }, }, }); + if (canceled) return; const res = buildSoundStore(result); if (res) soundSetting.value = res; - function buildSoundStore(result: any): SoundStore | null { - const type = (result.type === 'none' ? null : result.type) as SoundType; - const volume = result.volume as number; - const fileId = result.soundFile?.id ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileId : undefined); - const fileUrl = result.soundFile?.url ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileUrl : undefined); + function buildSoundStore(r: NonNullable): SoundStore | null { + const type = (r.type === 'none' ? null : r.type); + const volume = r.volume; + const fileId = r.soundFile?.id ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileId : undefined); + const fileUrl = r.soundFile?.url ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileUrl : undefined); if (type === '_driveFile_') { if (!fileUrl || !fileId) { diff --git a/packages/frontend/src/utility/form.ts b/packages/frontend/src/utility/form.ts index 1032e97ac9..2b765dc714 100644 --- a/packages/frontend/src/utility/form.ts +++ b/packages/frontend/src/utility/form.ts @@ -5,55 +5,59 @@ import * as Misskey from 'misskey-js'; -type EnumItem = string | { +export type EnumItem = string | { label: string; - value: string; + value: unknown; }; type Hidden = boolean | ((v: any) => boolean); -export type FormItem = { +interface FormItemBase { label?: string; + hidden?: Hidden; +} + +export interface StringFormItem extends FormItemBase { type: 'string'; default?: string | null; description?: string; required?: boolean; - hidden?: Hidden; multiline?: boolean; treatAsMfm?: boolean; -} | { - label?: string; +} + +export interface NumberFormItem extends FormItemBase { type: 'number'; default?: number | null; description?: string; required?: boolean; - hidden?: Hidden; step?: number; -} | { - label?: string; +} + +export interface BooleanFormItem extends FormItemBase { type: 'boolean'; default?: boolean | null; description?: string; - hidden?: Hidden; -} | { - label?: string; +} + +export interface EnumFormItem extends FormItemBase { type: 'enum'; default?: string | null; required?: boolean; - hidden?: Hidden; enum: EnumItem[]; -} | { - label?: string; +} + +export interface RadioFormItem extends FormItemBase { type: 'radio'; default?: unknown | null; required?: boolean; - hidden?: Hidden; options: { label: string; value: unknown; }[]; -} | { - label?: string; +} + +export interface RangeFormItem extends FormItemBase { type: 'range'; default?: number | null; description?: string; @@ -62,42 +66,80 @@ export type FormItem = { min: number; max: number; textConverter?: (value: number) => string; - hidden?: Hidden; -} | { - label?: string; +} + +export interface ObjectFormItem extends FormItemBase { type: 'object'; default?: Record | null; - hidden: Hidden; -} | { - label?: string; +} + +export interface ArrayFormItem extends FormItemBase { type: 'array'; default?: unknown[] | null; - hidden: Hidden; -} | { +} + +export interface ButtonFormItem extends FormItemBase { type: 'button'; content?: string; - hidden?: Hidden; action: (ev: MouseEvent, v: any) => void; -} | { +} + +export interface DriveFileFormItem extends FormItemBase { type: 'drive-file'; defaultFileId?: string | null; - hidden?: Hidden; validate?: (v: Misskey.entities.DriveFile) => Promise; -}; +} + +export type FormItem = + StringFormItem | + NumberFormItem | + BooleanFormItem | + EnumFormItem | + RadioFormItem | + RangeFormItem | + ObjectFormItem | + ArrayFormItem | + ButtonFormItem | + DriveFileFormItem; export type Form = Record; +export type FormItemWithDefault = FormItem & { + default: unknown; +}; + +export type FormWithDefault = Record; + +type GetRadioItemType = Item['options'][number]['value']; +type GetEnumItemType = E extends { value: unknown } ? E['value'] : E; + +type InferDefault = T extends { default: infer D } + ? D extends undefined ? Fallback : D + : Fallback; + +type NonNullableIfRequired = + Item extends { required: false } ? T | null | undefined : NonNullable; + type GetItemType = - Item['type'] extends 'string' ? string : - Item['type'] extends 'number' ? number : - Item['type'] extends 'boolean' ? boolean : - Item['type'] extends 'radio' ? unknown : - Item['type'] extends 'range' ? number : - Item['type'] extends 'enum' ? string : - Item['type'] extends 'array' ? unknown[] : - Item['type'] extends 'object' ? Record : - Item['type'] extends 'drive-file' ? Misskey.entities.DriveFile | undefined : - never; + Item extends StringFormItem + ? NonNullableIfRequired, Item> + : Item extends NumberFormItem + ? NonNullableIfRequired, Item> + : Item extends BooleanFormItem + ? boolean + : Item extends RadioFormItem + ? GetRadioItemType + : Item extends RangeFormItem + ? NonNullableIfRequired, Item> + : Item extends EnumFormItem + ? GetEnumItemType + : Item extends ArrayFormItem + ? NonNullableIfRequired, Item> + : Item extends ObjectFormItem + ? NonNullableIfRequired>, Item> + : Item extends DriveFileFormItem + ? Misskey.entities.DriveFile | undefined + : never; export type GetFormResultType = { [P in keyof F]: GetItemType; diff --git a/packages/frontend/src/utility/get-note-menu.ts b/packages/frontend/src/utility/get-note-menu.ts index ea93444f08..5361c1252d 100644 --- a/packages/frontend/src/utility/get-note-menu.ts +++ b/packages/frontend/src/utility/get-note-menu.ts @@ -101,7 +101,7 @@ export async function getNoteClipMenu(props: { const { canceled, result } = await os.form(i18n.ts.createNewClip, { name: { type: 'string', - default: null, + default: null as string | null, label: i18n.ts.name, }, description: { diff --git a/packages/frontend/src/utility/get-user-menu.ts b/packages/frontend/src/utility/get-user-menu.ts index 5c08b8c462..ad0864019b 100644 --- a/packages/frontend/src/utility/get-user-menu.ts +++ b/packages/frontend/src/utility/get-user-menu.ts @@ -132,6 +132,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router const userDetailed = await misskeyApi('users/show', { userId: user.id, }); + const { canceled, result } = await os.form(i18n.ts.editMemo, { memo: { type: 'string', @@ -141,6 +142,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router default: userDetailed.memo, }, }); + if (canceled) return; os.apiWithDialog('users/update-memo', { diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue index db03d1406c..9625abb4d1 100644 --- a/packages/frontend/src/widgets/WidgetActivity.vue +++ b/packages/frontend/src/widgets/WidgetActivity.vue @@ -25,29 +25,31 @@ import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js'; import XCalendar from './WidgetActivity.calendar.vue'; import XChart from './WidgetActivity.chart.vue'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import { misskeyApiGet } from '@/utility/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; -import { $i } from '@/i.js'; +import { ensureSignin } from '@/i.js'; import { i18n } from '@/i18n.js'; +const $i = ensureSignin(); + const name = 'activity'; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, transparent: { - type: 'boolean' as const, + type: 'boolean', default: false, }, view: { - type: 'number' as const, + type: 'number', default: 0, hidden: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType; diff --git a/packages/frontend/src/widgets/WidgetAichan.vue b/packages/frontend/src/widgets/WidgetAichan.vue index 2bc7facc88..3951de1d84 100644 --- a/packages/frontend/src/widgets/WidgetAichan.vue +++ b/packages/frontend/src/widgets/WidgetAichan.vue @@ -13,16 +13,16 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, onUnmounted, useTemplateRef } from 'vue'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; const name = 'ai'; const widgetPropsDef = { transparent: { - type: 'boolean' as const, + type: 'boolean', default: false, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType; @@ -42,6 +42,8 @@ const touched = () => { }; const onMousemove = (ev: MouseEvent) => { + if (!live2d.value || !live2d.value.contentWindow) return; + const iframeRect = live2d.value.getBoundingClientRect(); live2d.value.contentWindow.postMessage({ type: 'moveCursor', diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue index da71dcad29..a2d964718e 100644 --- a/packages/frontend/src/widgets/WidgetAiscript.vue +++ b/packages/frontend/src/widgets/WidgetAiscript.vue @@ -23,7 +23,7 @@ import { ref } from 'vue'; import { Interpreter, Parser, utils } from '@syuilo/aiscript'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import * as os from '@/os.js'; import MkContainer from '@/components/MkContainer.vue'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; @@ -35,16 +35,16 @@ const name = 'aiscript'; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, script: { - type: 'string' as const, + type: 'string', multiline: true, default: '(1 + 1)', hidden: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType; @@ -106,7 +106,7 @@ const run = async () => { } catch (err) { os.alert({ type: 'error', - text: err, + text: err instanceof Error ? err.message : String(err), }); } }; diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue index 429b0e0ffb..fdd4eaae06 100644 --- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue +++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue @@ -18,7 +18,7 @@ import type { Ref } from 'vue'; import { Interpreter, Parser } from '@syuilo/aiscript'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import * as os from '@/os.js'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; import { $i } from '@/i.js'; @@ -31,15 +31,15 @@ const name = 'aiscriptApp'; const widgetPropsDef = { script: { - type: 'string' as const, + type: 'string', multiline: true, default: '', }, showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType; @@ -92,7 +92,7 @@ async function run() { os.alert({ type: 'error', title: 'AiScript Error', - text: err.message, + text: err instanceof Error ? err.message : String(err), }); } } diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue index 4790f143cb..d1991cd70a 100644 --- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
@@ -27,7 +27,7 @@ import * as Misskey from 'misskey-js'; import { useInterval } from '@@/js/use-interval.js'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; @@ -37,10 +37,10 @@ const name = i18n.ts._widgets.birthdayFollowings; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType; diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue index 4afe735a22..e88a960f87 100644 --- a/packages/frontend/src/widgets/WidgetButton.vue +++ b/packages/frontend/src/widgets/WidgetButton.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { Interpreter, Parser } from '@syuilo/aiscript'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import * as os from '@/os.js'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; import { $i } from '@/i.js'; @@ -25,19 +25,19 @@ const name = 'button'; const widgetPropsDef = { label: { - type: 'string' as const, + type: 'string', default: 'BUTTON', }, colored: { - type: 'boolean' as const, + type: 'boolean', default: true, }, script: { - type: 'string' as const, + type: 'string', multiline: true, default: 'Mk:dialog("hello" "world")', }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType; @@ -81,7 +81,7 @@ const run = async () => { } catch (err) { os.alert({ type: 'error', - text: err, + text: err instanceof Error ? err.message : String(err), }); } }; diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue index 54f78469b2..12c0a66c5c 100644 --- a/packages/frontend/src/widgets/WidgetCalendar.vue +++ b/packages/frontend/src/widgets/WidgetCalendar.vue @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import { i18n } from '@/i18n.js'; import { useInterval } from '@@/js/use-interval.js'; @@ -49,10 +49,10 @@ const name = 'calendar'; const widgetPropsDef = { transparent: { - type: 'boolean' as const, + type: 'boolean', default: false, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType; diff --git a/packages/frontend/src/widgets/WidgetChat.vue b/packages/frontend/src/widgets/WidgetChat.vue index 43b2a6e522..8fee7f00f6 100644 --- a/packages/frontend/src/widgets/WidgetChat.vue +++ b/packages/frontend/src/widgets/WidgetChat.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { } from 'vue'; import { useWidgetPropsManager } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; -import type { GetFormResultType } from '@/utility/form.js'; +import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { i18n } from '@/i18n.js'; import MkChatHistories from '@/components/MkChatHistories.vue'; @@ -28,10 +28,10 @@ const name = 'chat'; const widgetPropsDef = { showHeader: { - type: 'boolean' as const, + type: 'boolean', default: true, }, -}; +} satisfies FormWithDefault; type WidgetProps = GetFormResultType; diff --git a/packages/frontend/src/widgets/WidgetClicker.vue b/packages/frontend/src/widgets/WidgetClicker.vue index 87ffd3d732..282a1a6d93 100644 --- a/packages/frontend/src/widgets/WidgetClicker.vue +++ b/packages/frontend/src/widgets/WidgetClicker.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only