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" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e3c9dfc77..14f7fc5de6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Feat: ノートの下書き機能 - Feat: クリップ内でノートを検索できるように - Feat: Playを検索できるように +- Feat: モデレーションにおいて、特定のドライブファイルを添付しているチャットメッセージを一覧できるように ### Client - Feat: モデログを検索できるように @@ -13,13 +14,16 @@ - Enhance: 投稿フォームにファイルをペースト/ドロップした際のUXを改善 - Enhance: ページネーション(一覧表示)の並び順を逆にできるように - Enhance: ページネーション(一覧表示)の基準日時を指定できるように +- Enhance: レンダリングパフォーマンスの向上 - Fix: ファイルがドライブの既定アップロード先に指定したフォルダにアップロードされない問題を修正 - Fix: プラグインをアンインストールしてもセーブデータが残る問題を修正 - Fix: 数時間後Misskeyのタブに戻った際に、タブがスロットリングされている間の更新アニメーションを延々見せ続けられる問題を修正 - Fix: 非ログイン時のハイライトノートの画像がCWの有無を考慮せず表示される問題を修正 +- Fix: レンジ選択・ドロップダウンにて、操作を無効にすべきところで無効にならない問題を修正 ### Server - Enhance: sinceId/untilIdが指定可能なエンドポイントにおいて、sinceDate/untilDateも指定可能に +- Enhance: メールの送信者としてサーバー名を表示するように (サーバー名が設定されている場合) - Fix: ジョブキューのProgressの値を正しく計算する diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 4b9c3ffa38..9be8caf6bb 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -3182,7 +3182,7 @@ drafts: "Esborrany " _drafts: select: "Seleccionar esborrany" cannotCreateDraftAnymore: "S'ha sobrepassat el nombre màxim d'esborranys que es poden crear." - cannotCreateDraftOfRenote: "No es poden crear esborranys de remotes." + cannotCreateDraft: "Amb aquest contingut no es poden crear esborranys." delete: "Esborrar esborranys" deleteAreYouSure: "Vols esborrar els esborranys?" noDrafts: "No hi ha esborranys" diff --git a/locales/en-US.yml b/locales/en-US.yml index f55c6c6763..6459fedda0 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -3182,7 +3182,6 @@ drafts: "Drafts" _drafts: select: "Select Draft" cannotCreateDraftAnymore: "The number of drafts that can be created has been exceeded." - cannotCreateDraftOfRenote: "You cannot create a draft of a renote." delete: "Delete Draft" deleteAreYouSure: "Delete draft?" noDrafts: "No drafts" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 262cc42c82..fc5f6c9ef5 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -3182,7 +3182,7 @@ drafts: "Borrador" _drafts: select: "Seleccionar borradores" cannotCreateDraftAnymore: "Se ha superado el número de borradores que se pueden crear." - cannotCreateDraftOfRenote: "No se pueden crear borradores de renotas." + cannotCreateDraft: "No se pueden crear borradores con este contenido." delete: "Eliminar borrador" deleteAreYouSure: "¿Quieres borrar el borrador?" noDrafts: "No hay borradores disponibles." diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 0e8e46dc7b..fb8713d3b3 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -5,9 +5,13 @@ introMisskey: "Selamat datang! Misskey adalah perangkat mikroblog tercatu bersif poweredByMisskeyDescription: "{name} adalah sebuah layanan (instance) yang menggunakan platform sumber terbuka Misskey." monthAndDay: "{day} {month}" search: "Penelusuran" +reset: "Reset" notifications: "Notifikasi" username: "Nama Pengguna" password: "Kata sandi" +initialPasswordForSetup: "Kata sandi untuk memulai konfigurasi awal" +initialPasswordIsIncorrect: "Kata sandi untuk memulai konfigurasi awal salah." +initialPasswordForSetupDescription: "Jika Anda menginstal Misskey sendiri, gunakan kata sandi yang Anda masukkan di berkas konfigurasi.\nJika Anda menggunakan layanan hosting Misskey, gunakan kata sandi yang diberikan.\nJika Anda belum mengatur kata sandi, biarkan kosong dan lanjutkan." forgotPassword: "Lupa Kata Sandi" fetchingAsApObject: "Mengambil data dari Fediverse..." ok: "OK" @@ -45,6 +49,7 @@ pin: "Sematkan ke profil" unpin: "Lepas sematan dari profil" copyContent: "Salin konten" copyLink: "Salin tautan" +copyRemoteLink: "Salin tautan jarak jauh" copyLinkRenote: "Salin tautan renote" delete: "Hapus" deleteAndEdit: "Hapus dan sunting" @@ -212,8 +217,10 @@ perDay: "per Hari" stopActivityDelivery: "Berhenti mengirim aktivitas" blockThisInstance: "Blokir instansi ini" silenceThisInstance: "Senyapkan instansi ini" +mediaSilenceThisInstance: "Server media senyap" operations: "Tindakan" software: "Perangkat lunak" +softwareName: "Nama Perangkat Lunak" version: "Versi" metadata: "Metadata" withNFiles: "{n} berkas" @@ -1040,7 +1047,7 @@ disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi disableFederationOk: "Matikan federasi" invitationRequiredToRegister: "Instansi ini dalam mode undangan-saja. Kamu harus memasukkan kode undangan yang valid untuk mendaftar." emailNotSupported: "Instansi ini tidak mendukung mengirim surel" -postToTheChannel: "Catat ke kanal" +postToTheChannel: "Buat Catatan ke Kanal" cannotBeChangedLater: "Hal ini nantinya tidak dapat diubah lagi." reactionAcceptance: "Penerimaan reaksi" likeOnly: "Hanya suka" @@ -2400,7 +2407,7 @@ _deck: main: "Utama" widgets: "Widget" notifications: "Notifikasi" - tl: "Lini masa" + tl: "Beranda" antenna: "Antena" list: "Daftar" channel: "Kanal" 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/locales/ko-KR.yml b/locales/ko-KR.yml index 458f75dfea..e86065ebb3 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -3182,7 +3182,6 @@ drafts: "초안" _drafts: select: "초안 선택" cannotCreateDraftAnymore: "초안 작성 가능 수를 초과했습니다." - cannotCreateDraftOfRenote: "리노트 초안은 작성할 수 없습니다." delete: "초안 삭제\n" deleteAreYouSure: "초안을 삭제하시겠습니까?" noDrafts: "초안 없음\n" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 213313349c..0fa88fec93 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -3182,6 +3182,5 @@ drafts: "Rascunhos" _drafts: select: "Selecionar Rascunho" cannotCreateDraftAnymore: "O número máximo de rascunhos foi excedido." - cannotCreateDraftOfRenote: "Você não pode criar o rascunho de uma repostagem." delete: "Excluir Rascunho" restore: "Redefinir" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index c49be193f2..3ab1f2e45a 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -3182,7 +3182,7 @@ drafts: "草稿" _drafts: select: "选择草稿" cannotCreateDraftAnymore: "已超过可创建的草稿数量。" - cannotCreateDraftOfRenote: "无法创建转帖草稿。" + cannotCreateDraft: "此内容无法创建草稿。" delete: "删除草稿" deleteAreYouSure: "要删除草稿吗?" noDrafts: "没有草稿" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 617e8ef0c7..4982dd093c 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -3182,7 +3182,7 @@ drafts: "草稿\n" _drafts: select: "選擇草槁" cannotCreateDraftAnymore: "已超出可建立的草稿數量上限。\n" - cannotCreateDraftOfRenote: "無法建立轉發的草稿。\n" + cannotCreateDraft: "無法以此內容建立草稿。\n" delete: "刪除草稿" deleteAreYouSure: "確定要刪除草稿嗎?\n" noDrafts: "沒有草稿。\n" diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 45d7ea11e4..c7be0f7843 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -145,7 +145,10 @@ export class EmailService { try { // TODO: htmlサニタイズ const info = await transporter.sendMail({ - from: this.meta.email!, + from: this.meta.name ? { + name: this.meta.name, + address: this.meta.email!, + } : this.meta.email!, to: to, subject: subject, text: text, diff --git a/packages/backend/src/models/json-schema/flash.ts b/packages/backend/src/models/json-schema/flash.ts index 42b2172409..d50200e6e9 100644 --- a/packages/backend/src/models/json-schema/flash.ts +++ b/packages/backend/src/models/json-schema/flash.ts @@ -51,7 +51,7 @@ export const packedFlashSchema = { }, likedCount: { type: 'number', - optional: false, nullable: true, + optional: false, nullable: false, }, isLiked: { type: 'boolean', 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/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'), '❤'); }); }); diff --git a/packages/frontend/src/components/MkDateSeparatedList.stories.impl.ts b/packages/frontend/src/components/MkDateSeparatedList.stories.impl.ts deleted file mode 100644 index 0e5635754c..0000000000 --- a/packages/frontend/src/components/MkDateSeparatedList.stories.impl.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import MkDateSeparatedList from './MkDateSeparatedList.vue'; -void MkDateSeparatedList; diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue deleted file mode 100644 index 82561055bc..0000000000 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ /dev/null @@ -1,254 +0,0 @@ - - - - - - - - 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/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue index 62ff806096..fa22a150c3 100644 --- a/packages/frontend/src/components/MkInstanceTicker.vue +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -12,7 +12,6 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -84,6 +60,7 @@ $height: 2ex; height: $height; border-radius: 4px 0 0 4px; overflow: clip; + color: #fff; // text-shadowは重いから使うな @@ -106,5 +83,10 @@ $height: 2ex; font-weight: bold; white-space: nowrap; overflow: visible; + + // text-shadowは重いから使うな + color: var(--MI_THEME-fg); + -webkit-text-stroke: var(--MI_THEME-panel) .225em; + paint-order: stroke fill; } diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 794a091f30..0605030d5b 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -729,7 +729,7 @@ function emitUpdReaction(emoji: string, delta: number) { } &:hover > .article > .main > .footer > .footerButton { - opacity: 1; + color: var(--MI_THEME-fg); } &.showActionsOnlyHover { @@ -1004,7 +1004,7 @@ function emitUpdReaction(emoji: string, delta: number) { .footerButton { margin: 0; padding: 8px; - opacity: 0.7; + color: color-mix(in srgb, var(--MI_THEME-panel), var(--MI_THEME-fg) 70%); // opacityなど不透明度で表現するとレンダリングパフォーマンスに影響するので通常の色の混合で代用 &:not(:last-child) { margin-right: 28px; @@ -1018,7 +1018,6 @@ function emitUpdReaction(emoji: string, delta: number) { .footerButtonCount { display: inline; margin: 0 0 0 8px; - opacity: 0.7; } @container (max-width: 580px) { diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index b0638db785..98247f5d0f 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -56,10 +56,12 @@ const emit = defineEmits<{ }>(); function getScreenY(event: TouchEvent | MouseEvent | PointerEvent): number { - if (event.touches && event.touches[0] && event.touches[0].screenY != null) { + if (('touches' in event) && event.touches[0] && event.touches[0].screenY != null) { return event.touches[0].screenY; - } else { + } else if ('screenY' in event) { return event.screenY; + } else { + return 0; // TSを黙らせるため } } diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue index 7a5848de48..67a9094cad 100644 --- a/packages/frontend/src/components/MkRange.vue +++ b/packages/frontend/src/components/MkRange.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -180,6 +180,8 @@ function onMouseenter() { let lastClickTime: number | null = null; function onMousedown(ev: MouseEvent | TouchEvent) { + if (props.disabled) return; // Prevent interaction if disabled + ev.preventDefault(); tooltipForDragShowing.value = true; @@ -292,6 +294,11 @@ function onMousedown(ev: MouseEvent | TouchEvent) { border: solid 1px var(--MI_THEME-panel); border-radius: 6px; + &.disabled { + pointer-events: none; + opacity: 0.6; + } + > .container { flex: 1; position: relative; diff --git a/packages/frontend/src/components/MkReactionIcon.vue b/packages/frontend/src/components/MkReactionIcon.vue index 36d1103549..7d62456e03 100644 --- a/packages/frontend/src/components/MkReactionIcon.vue +++ b/packages/frontend/src/components/MkReactionIcon.vue @@ -24,6 +24,7 @@ const elRef = useTemplateRef('elRef'); if (props.withTooltip) { useTooltip(elRef, (showing) => { + if (elRef.value == null) return; const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), { showing, reaction: props.reaction.replace(/^:(\w+):$/, ':$1@.:'), diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue index 3f14c5b5e0..15149b3f0c 100644 --- a/packages/frontend/src/components/MkRolePreview.vue +++ b/packages/frontend/src/components/MkRolePreview.vue @@ -41,7 +41,7 @@ import { i18n } from '@/i18n.js'; const props = withDefaults(defineProps<{ role: Misskey.entities.Role; forModeration: boolean; - detailed: boolean; + detailed?: boolean; }>(), { detailed: true, }); diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 58a4edfddf..485d163ac4 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -174,7 +174,7 @@ watch([modelValue, () => props.items], () => { }, { immediate: true, deep: true }); function show() { - if (opening.value) return; + if (opening.value || props.disabled || props.readonly) return; focus(); opening.value = true; diff --git a/packages/frontend/src/components/MkStreamingNotesTimeline.vue b/packages/frontend/src/components/MkStreamingNotesTimeline.vue index 44f873b6e3..693f551ffc 100644 --- a/packages/frontend/src/components/MkStreamingNotesTimeline.vue +++ b/packages/frontend/src/components/MkStreamingNotesTimeline.vue @@ -63,6 +63,7 @@ import { useDocumentVisibility } from '@@/js/use-document-visibility.js'; import { getScrollContainer, scrollToTop } from '@@/js/scroll.js'; import type { BasicTimelineType } from '@/timelines.js'; import type { SoundStore } from '@/preferences/def.js'; +import type { IPaginator, MisskeyEntity } from '@/utility/paginator.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import { useStream } from '@/stream.js'; import * as sound from '@/utility/sound.js'; @@ -76,7 +77,6 @@ import { i18n } from '@/i18n.js'; import { globalEvents, useGlobalEvent } from '@/events.js'; import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js'; import { Paginator } from '@/utility/paginator.js'; -import type { IPaginator, MisskeyEntity } from '@/utility/paginator.js'; const props = withDefaults(defineProps<{ src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; @@ -524,7 +524,6 @@ defineExpose({ align-items: center; justify-content: center; gap: 1em; - opacity: 0.75; padding: 8px 8px; margin: 0 auto; border-bottom: solid 0.5px var(--MI_THEME-divider); diff --git a/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue b/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue index e21adab36c..0276b7eaee 100644 --- a/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue +++ b/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue @@ -46,8 +46,8 @@ import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup, mark import * as Misskey from 'misskey-js'; import { useInterval } from '@@/js/use-interval.js'; import { useDocumentVisibility } from '@@/js/use-document-visibility.js'; -import type { notificationTypes } from '@@/js/const.js'; import { getScrollContainer, scrollToTop } from '@@/js/scroll.js'; +import type { notificationTypes } from '@@/js/const.js'; import XNotification from '@/components/MkNotification.vue'; import MkNote from '@/components/MkNote.vue'; import { useStream } from '@/stream.js'; @@ -235,7 +235,6 @@ defineExpose({ align-items: center; justify-content: center; gap: 1em; - opacity: 0.75; padding: 8px 8px; margin: 0 auto; border-bottom: solid 0.5px var(--MI_THEME-divider); 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/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/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/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index 1da16b8923..914c495d7a 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -28,11 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent, ref } from 'vue'; import { toUnicode as decodePunycode } from 'punycode.js'; import { url as local } from '@@/js/config.js'; +import { maybeMakeRelative } from '@@/js/url.js'; +import type { MkABehavior } from '@/components/global/MkA.vue'; import * as os from '@/os.js'; import { useTooltip } from '@/composables/use-tooltip.js'; import { isEnabledUrlPreview } from '@/utility/url-preview.js'; -import type { MkABehavior } from '@/components/global/MkA.vue'; -import { maybeMakeRelative } from '@@/js/url.js'; function safeURIDecode(str: string): string { try { @@ -94,7 +94,7 @@ const target = self ? null : '_blank'; } .schema { - opacity: 0.5; + color: color(from currentcolor srgb r g b / 0.5); // DOMノード全体をopacityで半透明化するより文字色を半透明化した方が若干レンダリングパフォーマンスが良い } .hostname { @@ -102,11 +102,11 @@ const target = self ? null : '_blank'; } .pathname { - opacity: 0.8; + color: color(from currentcolor srgb r g b / 0.8); // DOMノード全体をopacityで半透明化するより文字色を半透明化した方が若干レンダリングパフォーマンスが良い } .query { - opacity: 0.5; + color: color(from currentcolor srgb r g b / 0.5); // DOMノード全体をopacityで半透明化するより文字色を半透明化した方が若干レンダリングパフォーマンスが良い } .hash { 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/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index 72a3313c95..057deec4cf 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name }) }} + {{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name ?? host }) }} @@ -134,7 +134,7 @@ SPDX-License-Identifier: AGPL-3.0-only 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/frontend/src/pages/chat/message.vue b/packages/frontend/src/pages/chat/message.vue index a04ec7fd87..834aa9e033 100644 --- a/packages/frontend/src/pages/chat/message.vue +++ b/packages/frontend/src/pages/chat/message.vue @@ -25,7 +25,7 @@ import { misskeyApi } from '@/utility/misskey-api.js'; import { definePage } from '@/page.js'; const props = defineProps<{ - messageId?: string; + messageId: string; }>(); const initializing = ref(true); diff --git a/packages/frontend/src/pages/chat/room.vue b/packages/frontend/src/pages/chat/room.vue index ac13c5fac6..6443616fe3 100644 --- a/packages/frontend/src/pages/chat/room.vue +++ b/packages/frontend/src/pages/chat/room.vue @@ -197,7 +197,7 @@ async function initialize() { connection.value.on('deleted', onDeleted); connection.value.on('react', onReact); connection.value.on('unreact', onUnreact); - } else { + } else if (props.roomId) { const [rResult, mResult] = await Promise.allSettled([ misskeyApi('chat/rooms/show', { roomId: props.roomId }), misskeyApi('chat/messages/room-timeline', { roomId: props.roomId, limit: LIMIT }), 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/favorites.vue b/packages/frontend/src/pages/favorites.vue index 72dd2b4a16..b37b9c33c5 100644 --- a/packages/frontend/src/pages/favorites.vue +++ b/packages/frontend/src/pages/favorites.vue @@ -10,9 +10,7 @@ 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'; diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index 43632f55ca..74d8d639f3 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -67,7 +67,7 @@ const router = useRouter(); const tab = ref('featured'); const searchQuery = ref(''); -const searchPaginator = shallowRef(null); +const searchPaginator = shallowRef | null>(null); const searchKey = ref(0); const featuredFlashsPaginator = markRaw(new Paginator('flash/featured', { diff --git a/packages/frontend/src/pages/install-extensions.vue b/packages/frontend/src/pages/install-extensions.vue index 4e814ef84f..1b3c6616cc 100644 --- a/packages/frontend/src/pages/install-extensions.vue +++ b/packages/frontend/src/pages/install-extensions.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + @@ -151,7 +151,7 @@ async function fetch() { case 'theme': try { const metaRaw = parseThemeCode(res.data); - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, props, desc: description, ...meta } = metaRaw; data.value = { type: 'theme', diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 14a64f0bd5..4be5fa447d 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + {{ instance.name || `(${i18n.ts.unknown})` }}
diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue index 4368aff8be..1261428c1c 100644 --- a/packages/frontend/src/pages/list.vue +++ b/packages/frontend/src/pages/list.vue @@ -62,24 +62,29 @@ function fetchList(): void { } function like() { + if (list.value == null) return; os.apiWithDialog('users/lists/favorite', { listId: list.value.id, }).then(() => { + if (list.value == null) return; list.value.isLiked = true; list.value.likedCount++; }); } function unlike() { + if (list.value == null) return; os.apiWithDialog('users/lists/unfavorite', { listId: list.value.id, }).then(() => { + if (list.value == null) return; list.value.isLiked = false; list.value.likedCount--; }); } async function create() { + if (list.value == null) return; const { canceled, result: name } = await os.inputText({ title: i18n.ts.enterListName, }); 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/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
-