diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a15742dba7..0f3702f958 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,7 +17,7 @@ jobs: submodules: true - uses: pnpm/action-setup@v2 with: - version: 7 + version: 8 run_install: false - uses: actions/setup-node@v3.6.0 with: diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index f77daf5868..d5e6769bca 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -20,7 +20,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v2 with: - version: 7 + version: 8 run_install: false - name: Use Node.js 18.x uses: actions/setup-node@v3.6.0 diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index f1e414dbbc..d7be15bd4f 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -35,7 +35,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v2 with: - version: 7 + version: 8 run_install: false - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3.6.0 diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index b0da3769a7..4ea4ba4628 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -22,7 +22,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v2 with: - version: 7 + version: 8 run_install: false - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3.6.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 97ce4ec7b8..210b1ed5fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,12 +36,15 @@ - 1枚だけのメディアリストの画像のアスペクト比を画像に応じて縦長にするように - Fix: リアクションをホバーした時のユーザーリストで猫耳が切れてしまっていた問題を修正 - 新しい実績を追加 +- Renoteしたユーザーの一覧を見れるように ### Server +- 環境変数MISSKEY_CONFIG_YMLで設定ファイルをdefault.ymlから変更可能に - Fix: エクスポートデータの拡張子がunknownになる問題を修正 - Fix: Content-Dispositionのパースでエラーが発生した場合にダウンロードが完了しない問題を修正 - Fix: API: i/update avatarIdとbannerIdにnullを渡した時、画像がリセットされない問題を修正 - Fix: 1:1ではない画像のリアクション通知バッジが左や上に寄ってしまっていたのを中央に来るように修正 +- Fix: .wav, .flacが再生できない問題を修正(新しくアップロードされたファイルのみ修正が適用されます) ## 13.11.3 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 24a4b9fbb2..1e2019b8f0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1007,6 +1007,8 @@ accountMoved: "このユーザーは新しいアカウントに引っ越しま forceShowAds: "常に広告を表示する" addMemo: "メモを追加" editMemo: "メモを編集" +reactionsList: "リアクション一覧" +renotesList: "Renote一覧" notificationDisplay: "通知の表示" leftTop: "左上" rightTop: "右上" diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index e4f7601fa9..bb97d8c17c 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -4,7 +4,7 @@ import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; +import { dirname, resolve } from 'node:path'; import * as yaml from 'js-yaml'; /** @@ -132,10 +132,11 @@ const dir = `${_dirname}/../../../.config`; /** * Path of configuration file */ -const path = process.env.NODE_ENV === 'test' - ? `${dir}/test.yml` - : `${dir}/default.yml`; - +const path = process.env.MISSKEY_CONFIG_YML + ? resolve(dir, process.env.MISSKEY_CONFIG_YML) + : process.env.NODE_ENV === 'test' + ? resolve(dir, 'test.yml') + : resolve(dir, 'default.yml'); export function loadConfig() { const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8')); const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json'); diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index 6c7f214214..ee1a9a3093 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -56,6 +56,11 @@ export const FILE_TYPE_BROWSERSAFE = [ 'audio/webm', 'audio/aac', + + // see https://github.com/misskey-dev/misskey/pull/10686 + 'audio/flac', + 'audio/wav', + // backward compatibility 'audio/x-flac', 'audio/vnd.wave', ]; diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index e39b134b7e..b6cae5ea75 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -5,7 +5,7 @@ import * as stream from 'node:stream'; import * as util from 'node:util'; import { Injectable } from '@nestjs/common'; import { FSWatcher } from 'chokidar'; -import { fileTypeFromFile } from 'file-type'; +import * as fileType from 'file-type'; import FFmpeg from 'fluent-ffmpeg'; import isSvg from 'is-svg'; import probeImageSize from 'probe-image-size'; @@ -301,21 +301,34 @@ export class FileInfoService { return fs.promises.access(path).then(() => true, () => false); } + @bindThis + public fixMime(mime: string | fileType.MimeType): string { + // see https://github.com/misskey-dev/misskey/pull/10686 + if (mime === "audio/x-flac") { + return "audio/flac"; + } + if (mime === "audio/vnd.wave") { + return "audio/wav"; + } + + return mime; + } + /** * Detect MIME Type and extension */ @bindThis public async detectType(path: string): Promise<{ - mime: string; - ext: string | null; -}> { + mime: string; + ext: string | null; + }> { // Check 0 byte const fileSize = await this.getFileSize(path); if (fileSize === 0) { return TYPE_OCTET_STREAM; } - const type = await fileTypeFromFile(path); + const type = await fileType.fileTypeFromFile(path); if (type) { // XMLはSVGかもしれない @@ -324,7 +337,7 @@ export class FileInfoService { } return { - mime: type.mime, + mime: this.fixMime(type.mime), ext: type.ext, }; } diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index aa91d936b1..98329ddffa 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -454,7 +454,8 @@ export class FileServerService { fileRole: 'original', file, filename: file.name, - mime: file.type, + // 古いファイルは修正前のmimeを持っているのでできるだけ修正してあげる + mime: this.fileInfoService.fixMime(file.type), ext: null, path, }; diff --git a/packages/backend/test/resources/kick_gaba7.aac b/packages/backend/test/resources/kick_gaba7.aac new file mode 100644 index 0000000000..4644542f96 Binary files /dev/null and b/packages/backend/test/resources/kick_gaba7.aac differ diff --git a/packages/backend/test/resources/kick_gaba7.flac b/packages/backend/test/resources/kick_gaba7.flac new file mode 100644 index 0000000000..7512812018 Binary files /dev/null and b/packages/backend/test/resources/kick_gaba7.flac differ diff --git a/packages/backend/test/resources/kick_gaba7.mp3 b/packages/backend/test/resources/kick_gaba7.mp3 new file mode 100644 index 0000000000..6ba317deb1 Binary files /dev/null and b/packages/backend/test/resources/kick_gaba7.mp3 differ diff --git a/packages/backend/test/resources/kick_gaba7.wav b/packages/backend/test/resources/kick_gaba7.wav new file mode 100644 index 0000000000..2cd280148e Binary files /dev/null and b/packages/backend/test/resources/kick_gaba7.wav differ diff --git a/packages/backend/test/resources/kick_gaba7.webm b/packages/backend/test/resources/kick_gaba7.webm new file mode 100644 index 0000000000..82c5349cd4 Binary files /dev/null and b/packages/backend/test/resources/kick_gaba7.webm differ diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts index d05833560d..f378184c74 100644 --- a/packages/backend/test/unit/FileInfoService.ts +++ b/packages/backend/test/unit/FileInfoService.ts @@ -7,10 +7,10 @@ import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; import { GlobalModule } from '@/GlobalModule.js'; import { FileInfoService } from '@/core/FileInfoService.js'; -import { DI } from '@/di-symbols.js'; +//import { DI } from '@/di-symbols.js'; import { AiService } from '@/core/AiService.js'; import type { TestingModule } from '@nestjs/testing'; -import type { jest } from '@jest/globals'; +import { describe, beforeAll, afterAll, test } from '@jest/globals'; import type { MockFunctionMetadata } from 'jest-mock'; const _filename = fileURLToPath(import.meta.url); @@ -74,164 +74,271 @@ describe('FileInfoService', () => { }); }); - test('Generic JPEG', async () => { - const path = `${resources}/Lenna.jpg`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 25360, - md5: '091b3f259662aa31e2ffef4519951168', - type: { - mime: 'image/jpeg', - ext: 'jpg', - }, - width: 512, - height: 512, - orientation: undefined, + describe('IMAGE', () => { + test('Generic JPEG', async () => { + const path = `${resources}/Lenna.jpg`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 25360, + md5: '091b3f259662aa31e2ffef4519951168', + type: { + mime: 'image/jpeg', + ext: 'jpg', + }, + width: 512, + height: 512, + orientation: undefined, + }); + }); + + test('Generic APNG', async () => { + const path = `${resources}/anime.png`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 1868, + md5: '08189c607bea3b952704676bb3c979e0', + type: { + mime: 'image/apng', + ext: 'apng', + }, + width: 256, + height: 256, + orientation: undefined, + }); + }); + + test('Generic AGIF', async () => { + const path = `${resources}/anime.gif`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 2248, + md5: '32c47a11555675d9267aee1a86571e7e', + type: { + mime: 'image/gif', + ext: 'gif', + }, + width: 256, + height: 256, + orientation: undefined, + }); + }); + + test('PNG with alpha', async () => { + const path = `${resources}/with-alpha.png`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 3772, + md5: 'f73535c3e1e27508885b69b10cf6e991', + type: { + mime: 'image/png', + ext: 'png', + }, + width: 256, + height: 256, + orientation: undefined, + }); + }); + + test('Generic SVG', async () => { + const path = `${resources}/image.svg`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 505, + md5: 'b6f52b4b021e7b92cdd04509c7267965', + type: { + mime: 'image/svg+xml', + ext: 'svg', + }, + width: 256, + height: 256, + orientation: undefined, + }); + }); + + test('SVG with XML definition', async () => { + // https://github.com/misskey-dev/misskey/issues/4413 + const path = `${resources}/with-xml-def.svg`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 544, + md5: '4b7a346cde9ccbeb267e812567e33397', + type: { + mime: 'image/svg+xml', + ext: 'svg', + }, + width: 256, + height: 256, + orientation: undefined, + }); + }); + + test('Dimension limit', async () => { + const path = `${resources}/25000x25000.png`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 75933, + md5: '268c5dde99e17cf8fe09f1ab3f97df56', + type: { + mime: 'application/octet-stream', // do not treat as image + ext: null, + }, + width: 25000, + height: 25000, + orientation: undefined, + }); + }); + + test('Rotate JPEG', async () => { + const path = `${resources}/rotate.jpg`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + assert.deepStrictEqual(info, { + size: 12624, + md5: '68d5b2d8d1d1acbbce99203e3ec3857e', + type: { + mime: 'image/jpeg', + ext: 'jpg', + }, + width: 512, + height: 256, + orientation: 8, + }); }); }); - test('Generic APNG', async () => { - const path = `${resources}/anime.png`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 1868, - md5: '08189c607bea3b952704676bb3c979e0', - type: { - mime: 'image/apng', - ext: 'apng', - }, - width: 256, - height: 256, - orientation: undefined, + describe('AUDIO', () => { + test('MP3', async () => { + const path = `${resources}/kick_gaba7.mp3`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + delete info.width; + delete info.height; + delete info.orientation; + assert.deepStrictEqual(info, { + size: 19853, + md5: '4f557df8548bc3cecc794c652f690446', + type: { + mime: 'audio/mpeg', + ext: 'mp3', + }, + }); }); - }); - - test('Generic AGIF', async () => { - const path = `${resources}/anime.gif`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 2248, - md5: '32c47a11555675d9267aee1a86571e7e', - type: { - mime: 'image/gif', - ext: 'gif', - }, - width: 256, - height: 256, - orientation: undefined, + + test('WAV', async () => { + const path = `${resources}/kick_gaba7.wav`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + delete info.width; + delete info.height; + delete info.orientation; + assert.deepStrictEqual(info, { + size: 87630, + md5: '8bc9bb4fe5e77bb1871448209be635c1', + type: { + mime: 'audio/wav', + ext: 'wav', + }, + }); }); - }); - - test('PNG with alpha', async () => { - const path = `${resources}/with-alpha.png`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 3772, - md5: 'f73535c3e1e27508885b69b10cf6e991', - type: { - mime: 'image/png', - ext: 'png', - }, - width: 256, - height: 256, - orientation: undefined, + + test('AAC', async () => { + const path = `${resources}/kick_gaba7.aac`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + delete info.width; + delete info.height; + delete info.orientation; + assert.deepStrictEqual(info, { + size: 7291, + md5: '2789323f05e3392b648066f50be6a2a6', + type: { + mime: 'audio/aac', + ext: 'aac', + }, + }); }); - }); - - test('Generic SVG', async () => { - const path = `${resources}/image.svg`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 505, - md5: 'b6f52b4b021e7b92cdd04509c7267965', - type: { - mime: 'image/svg+xml', - ext: 'svg', - }, - width: 256, - height: 256, - orientation: undefined, + + test('FLAC', async () => { + const path = `${resources}/kick_gaba7.flac`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + delete info.width; + delete info.height; + delete info.orientation; + assert.deepStrictEqual(info, { + size: 108793, + md5: 'bc0f3adfe0e1ca99ae6c7528c46b3173', + type: { + mime: 'audio/flac', + ext: 'flac', + }, + }); }); - }); - - test('SVG with XML definition', async () => { - // https://github.com/misskey-dev/misskey/issues/4413 - const path = `${resources}/with-xml-def.svg`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 544, - md5: '4b7a346cde9ccbeb267e812567e33397', - type: { - mime: 'image/svg+xml', - ext: 'svg', - }, - width: 256, - height: 256, - orientation: undefined, - }); - }); - - test('Dimension limit', async () => { - const path = `${resources}/25000x25000.png`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 75933, - md5: '268c5dde99e17cf8fe09f1ab3f97df56', - type: { - mime: 'application/octet-stream', // do not treat as image - ext: null, - }, - width: 25000, - height: 25000, - orientation: undefined, - }); - }); - - test('Rotate JPEG', async () => { - const path = `${resources}/rotate.jpg`; - const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 12624, - md5: '68d5b2d8d1d1acbbce99203e3ec3857e', - type: { - mime: 'image/jpeg', - ext: 'jpg', - }, - width: 512, - height: 256, - orientation: 8, + + /* + * video/webmとして検出されてしまう + test('WEBM AUDIO', async () => { + const path = `${resources}/kick_gaba7.webm`; + const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; + delete info.warnings; + delete info.blurhash; + delete info.sensitive; + delete info.porn; + delete info.width; + delete info.height; + delete info.orientation; + assert.deepStrictEqual(info, { + size: 8879, + md5: '3350083dec312419cfdc06c16413aca7', + type: { + mime: 'audio/webm', + ext: 'webm', + }, + }); }); + */ }); }); diff --git a/packages/frontend/src/components/MkReactedUsersDialog.vue b/packages/frontend/src/components/MkReactedUsersDialog.vue index 1506e24ce8..0c0cc36692 100644 --- a/packages/frontend/src/components/MkReactedUsersDialog.vue +++ b/packages/frontend/src/components/MkReactedUsersDialog.vue @@ -6,7 +6,7 @@ @close="dialog.close()" @closed="emit('closed')" > - +
@@ -21,7 +21,7 @@ {{ note.reactions[reaction] }}
- + diff --git a/packages/frontend/src/components/MkRenotedUsersDialog.vue b/packages/frontend/src/components/MkRenotedUsersDialog.vue new file mode 100644 index 0000000000..56025535f1 --- /dev/null +++ b/packages/frontend/src/components/MkRenotedUsersDialog.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index 1d1b8fcea4..38af9eac9a 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -35,6 +35,11 @@ export const FILE_TYPE_BROWSERSAFE = [ 'audio/webm', 'audio/aac', + + // see https://github.com/misskey-dev/misskey/pull/10686 + 'audio/flac', + 'audio/wav', + // backward compatibility 'audio/x-flac', 'audio/vnd.wave', ]; diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue index b742132af6..d51bf6230a 100644 --- a/packages/frontend/src/pages/admin/email-settings.vue +++ b/packages/frontend/src/pages/admin/email-settings.vue @@ -96,7 +96,9 @@ async function testEmail() { const { canceled, result: destination } = await os.inputText({ title: i18n.ts.destination, type: 'email', - placeholder: instance.maintainerEmail, + default: instance.maintainerEmail ?? '', + placeholder: 'test@example.com', + minLength: 1, }); if (canceled) return; os.apiWithDialog('admin/send-email', { diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index 35edcc7cda..639e351cac 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -305,6 +305,11 @@ const PRESET_TIMELINE = `/// @ 0.13.1 // それぞれのノートごとにUI要素作成 let noteEls = [] each (let note, notes) { + // 表示名を設定していないアカウントはidを表示 + let userName = if Core:type(note.user.name) == "str" note.user.name else note.user.username + // リノートもしくはメディア・投票のみで本文が無いノートに代替表示文を設定 + let noteText = if Core:type(note.text) == "str" note.text else "(リノートもしくはメディア・投票のみのノート)" + let el = Ui:C:container({ bgColor: "#444" fgColor: "#fff" @@ -312,11 +317,11 @@ const PRESET_TIMELINE = `/// @ 0.13.1 rounded: true children: [ Ui:C:mfm({ - text: note.user.name + text: userName bold: true }) Ui:C:mfm({ - text: note.text + text: noteText }) ] }) diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index d91f0b0eb6..c8a6100253 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -211,6 +211,12 @@ export function getNoteMenu(props: { }, {}, 'closed'); } + function showRenotes(): void { + os.popup(defineAsyncComponent(() => import('@/components/MkRenotedUsersDialog.vue')), { + noteId: appearNote.id, + }, {}, 'closed'); + } + async function translate(): Promise { if (props.translation.value != null) return; props.translating.value = true; @@ -241,8 +247,12 @@ export function getNoteMenu(props: { text: i18n.ts.details, action: openDetail, }, { - icon: 'ti ti-users', - text: i18n.ts.reactions, + icon: 'ti ti-repeat', + text: i18n.ts.renotesList, + action: showRenotes, + }, { + icon: 'ti ti-icons', + text: i18n.ts.reactionsList, action: showReactions, }, { icon: 'ti ti-copy',