diff --git a/.config/example.yml b/.config/example.yml index 7fea929374..b0b7f14059 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -38,7 +38,7 @@ # Option 3: If neither of the above applies to you. # (In this case, the source code should be published # on the Misskey interface. IT IS NOT ENOUGH TO -# DISCLOSE THE SOURCE CODE WEHN A USER REQUESTS IT BY +# DISCLOSE THE SOURCE CODE WHEN A USER REQUESTS IT BY # E-MAIL OR OTHER MEANS. If you are not satisfied # with this, it is recommended that you read the # license again carefully. Anyway, enabling this diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e409adf644..f8d9905ecd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -19,7 +19,6 @@ "editorconfig.editorconfig", "dbaeumer.vscode-eslint", "Vue.volar", - "Vue.vscode-typescript-vue-plugin", "Orta.vscode-jest", "dbaeumer.vscode-eslint", "mrmlnc.vscode-json5" diff --git a/.vscode/extensions.json b/.vscode/extensions.json index baca8db246..d08109477c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,7 +3,6 @@ "editorconfig.editorconfig", "dbaeumer.vscode-eslint", "Vue.volar", - "Vue.vscode-typescript-vue-plugin", "Orta.vscode-jest", "dbaeumer.vscode-eslint", "mrmlnc.vscode-json5" diff --git a/CHANGELOG.md b/CHANGELOG.md index f441dc1a91..20b093489c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,12 @@ * β版として公開のため、旧画面も引き続き利用可能です ### Client -- +- Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように +- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように +- Enhance: リアクション・いいねの総数を表示するように +- Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように +- Fix: 一部のページ内リンクが正しく動作しない問題を修正 +- Fix: 周年の実績が閏年を考慮しない問題を修正 ### Server - @@ -22,7 +27,7 @@ - Fix: カスタム絵文字の画像読み込みに失敗した際はテキストではなくダミー画像を表示 #13487 ### Server -- +- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに ## 2024.3.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3263bf6aa..dcb625626d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -316,6 +316,98 @@ export const handlers = [ Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files. +## Nest + +### Nest Service Circular dependency / Nestでサービスの循環参照でエラーが起きた場合 + +#### forwardRef +まずは簡単に`forwardRef`を試してみる + +```typescript +export class FooService { + constructor( + @Inject(forwardRef(() => BarService)) + private barService: BarService + ) { + } +} +``` + +#### OnModuleInit +できなければ`OnModuleInit`を使う + +```typescript +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import { BarService } from '@/core/BarService'; + +@Injectable() +export class FooService implements OnModuleInit { + private barService: BarService // constructorから移動してくる + + constructor( + private moduleRef: ModuleRef, + ) { + } + + async onModuleInit() { + this.barService = this.moduleRef.get(BarService.name); + } + + public async niceMethod() { + return await this.barService.incredibleMethod({ hoge: 'fuga' }); + } +} +``` + +##### Service Unit Test +テストで`onModuleInit`を呼び出す必要がある + +```typescript +// import ... + +describe('test', () => { + let app: TestingModule; + let fooService: FooService; // for test case + let barService: BarService; // for test case + + beforeEach(async () => { + app = await Test.createTestingModule({ + imports: ..., + providers: [ + FooService, + { // mockする (mockは必須ではないかもしれない) + provide: BarService, + useFactory: () => ({ + incredibleMethod: jest.fn(), + }), + }, + { // Provideにする + provide: BarService.name, + useExisting: BarService, + }, + ], + }) + .useMocker(... + .compile(); + + fooService = app.get(FooService); + barService = app.get(BarService) as jest.Mocked; + + // onModuleInitを実行する + await fooService.onModuleInit(); + }); + + test('nice', () => { + await fooService.niceMethod(); + + expect(barService.incredibleMethod).toHaveBeenCalled(); + expect(barService.incredibleMethod.mock.lastCall![0]) + .toEqual({ hoge: 'fuga' }); + }); +}) +``` + ## Notes ### Misskeyのドメイン固有の概念は`Mi`をprefixする diff --git a/locales/index.d.ts b/locales/index.d.ts index 2080b87543..d09f411114 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1996,6 +1996,10 @@ export interface Locale extends ILocale { * ノートのアクションをホバー時のみ表示する */ "showNoteActionsOnlyHover": string; + /** + * ノートのリアクション数を表示する + */ + "showReactionsCount": string; /** * 履歴はありません */ @@ -8913,6 +8917,10 @@ export interface Locale extends ILocale { * {n}人がリアクションしました */ "reactedBySomeUsers": ParameterizedString<"n">; + /** + * {n}人がいいねしました + */ + "likedBySomeUsers": ParameterizedString<"n">; /** * {n}人がリノートしました */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 55f0be33fa..a4d7ee7d2e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -495,6 +495,7 @@ emojiStyle: "絵文字のスタイル" native: "ネイティブ" disableDrawer: "メニューをドロワーで表示しない" showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する" +showReactionsCount: "ノートのリアクション数を表示する" noHistory: "履歴はありません" signinHistory: "ログイン履歴" enableAdvancedMfm: "高度なMFMを有効にする" @@ -2356,6 +2357,7 @@ _notification: sendTestNotification: "テスト通知を送信する" notificationWillBeDisplayedLikeThis: "通知はこのように表示されます" reactedBySomeUsers: "{n}人がリアクションしました" + likedBySomeUsers: "{n}人がいいねしました" renotedBySomeUsers: "{n}人がリノートしました" followedBySomeUsers: "{n}人にフォローされました" flushNotification: "通知の履歴をリセットする" diff --git a/packages/backend/package.json b/packages/backend/package.json index 8680610441..eaad96d5f6 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -19,7 +19,7 @@ "watch": "node watch.mjs", "restart": "pnpm build && pnpm start", "dev": "nodemon -w src -e ts,js,mjs,cjs,json --exec \"cross-env NODE_ENV=development pnpm run restart\"", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit && tsc -p test --noEmit", "eslint": "eslint --quiet \"src/**/*.ts\"", "lint": "pnpm typecheck && pnpm eslint", "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs", diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index b2fd435f93..4d64b08e15 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -129,7 +129,7 @@ export class ApNoteService { value, object, }); - throw new Error('invalid note'); + throw err; } const note = object as IPost; diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index 8affe2b3bf..26bf386cbc 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -248,7 +248,7 @@ export class DriveFileEntityService { folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { detail: true, }) : null, - userId: opts.withUser ? file.userId : null, + userId: file.userId, user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null, }); } diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 5b6affc6a5..22d01462e7 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -333,6 +333,7 @@ export class NoteEntityService implements OnModuleInit { visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined, renoteCount: note.renoteCount, repliesCount: note.repliesCount, + reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0), reactions: this.reactionService.convertLegacyReactions(note.reactions), reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host), reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined, diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index bb4ccc7ee4..2641161c8b 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -223,6 +223,10 @@ export const packedNoteSchema = { }], }, }, + reactionCount: { + type: 'number', + optional: false, nullable: false, + }, renoteCount: { type: 'number', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 459729f61f..76a34924a0 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -67,7 +67,7 @@ export const paramDef = { withFile: { type: 'boolean' }, notify: { type: 'boolean' }, }, - required: ['antennaId', 'name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], + required: ['antennaId'], } as const; @Injectable() @@ -83,8 +83,10 @@ export default class extends Endpoint { // eslint- private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { - throw new Error('either keywords or excludeKeywords is required.'); + if (ps.keywords && ps.excludeKeywords) { + if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { + throw new Error('either keywords or excludeKeywords is required.'); + } } // Fetch the antenna const antenna = await this.antennasRepository.findOneBy({ @@ -98,7 +100,7 @@ export default class extends Endpoint { // eslint- let userList; - if (ps.src === 'list' && ps.userListId) { + if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) { userList = await this.userListsRepository.findOneBy({ id: ps.userListId, userId: me.id, @@ -112,7 +114,7 @@ export default class extends Endpoint { // eslint- await this.antennasRepository.update(antenna.id, { name: ps.name, src: ps.src, - userListId: userList ? userList.id : null, + userListId: ps.userListId !== undefined ? userList ? userList.id : null : undefined, keywords: ps.keywords, excludeKeywords: ps.excludeKeywords, users: ps.users, diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 59441826b0..396536948e 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -86,8 +86,8 @@ //#endregion //#region Script - function importAppScript() { - import(`/vite/${CLIENT_ENTRY}`) + async function importAppScript() { + await import(`/vite/${CLIENT_ENTRY}`) .catch(async e => { console.error(e); renderError('APP_IMPORT', e); diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 973bcbd750..11016f58ae 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -472,7 +472,7 @@ describe('Note', () => { priority: 0, value: true, }, - }, + } as any, }, alice); assert.strictEqual(res.status, 200); @@ -784,7 +784,7 @@ describe('Note', () => { priority: 1, value: 0, }, - }, + } as any, }, alice); assert.strictEqual(res.status, 200); @@ -838,7 +838,7 @@ describe('Note', () => { priority: 1, value: 0, }, - }, + } as any, }, alice); assert.strictEqual(res.status, 200); @@ -894,7 +894,7 @@ describe('Note', () => { priority: 1, value: 1, }, - }, + } as any, }, alice); assert.strictEqual(res.status, 200); diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index d413703ede..5487292afc 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -890,17 +890,35 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); await sleep(1000); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id, withReplies: false }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); }); + test.concurrent('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); + await sleep(1000); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + }); + test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); diff --git a/packages/backend/test/global.d.ts b/packages/backend/test/global.d.ts new file mode 100644 index 0000000000..0363073356 --- /dev/null +++ b/packages/backend/test/global.d.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type FIXME = any; diff --git a/packages/backend/test/prelude/get-api-validator.ts b/packages/backend/test/prelude/get-api-validator.ts index b86a7a978d..7aa7a92702 100644 --- a/packages/backend/test/prelude/get-api-validator.ts +++ b/packages/backend/test/prelude/get-api-validator.ts @@ -4,10 +4,10 @@ */ import Ajv from 'ajv'; -import { Schema } from '@/misc/schema'; +import { Schema } from '@/misc/json-schema.js'; export const getValidator = (paramDef: Schema) => { - const ajv = new Ajv({ + const ajv = new Ajv.default({ useDefaults: true, }); ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json index 4597ff8780..2b562acda8 100644 --- a/packages/backend/test/tsconfig.json +++ b/packages/backend/test/tsconfig.json @@ -5,7 +5,7 @@ "noImplicitAny": true, "noImplicitReturns": true, "noUnusedParameters": false, - "noUnusedLocals": true, + "noUnusedLocals": false, "noFallthroughCasesInSwitch": true, "declaration": false, "sourceMap": true, @@ -18,6 +18,7 @@ "strict": true, "strictNullChecks": true, "strictPropertyInitialization": false, + "skipLibCheck": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "resolveJsonModule": true, diff --git a/packages/backend/test/unit/RelayService.ts b/packages/backend/test/unit/RelayService.ts index f2a67dba46..9676abf07b 100644 --- a/packages/backend/test/unit/RelayService.ts +++ b/packages/backend/test/unit/RelayService.ts @@ -90,7 +90,8 @@ describe('RelayService', () => { expect(queueService.deliver).toHaveBeenCalled(); expect(queueService.deliver.mock.lastCall![1]?.type).toBe('Undo'); - expect(queueService.deliver.mock.lastCall![1]?.object.type).toBe('Follow'); + expect(typeof queueService.deliver.mock.lastCall![1]?.object).toBe('object'); + expect((queueService.deliver.mock.lastCall![1]?.object as any).type).toBe('Follow'); expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com'); //expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor'); diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 61f04678bf..8016e8b0e0 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -187,14 +187,26 @@ export async function mainBoot() { if ($i.followersCount >= 500) claimAchievement('followers500'); if ($i.followersCount >= 1000) claimAchievement('followers1000'); - if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) { - claimAchievement('passedSinceAccountCreated1'); - } - if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) { - claimAchievement('passedSinceAccountCreated2'); - } - if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) { + const createdAt = new Date($i.createdAt); + const createdAtThreeYearsLater = new Date($i.createdAt); + createdAtThreeYearsLater.setFullYear(createdAtThreeYearsLater.getFullYear() + 3); + if (now >= createdAtThreeYearsLater) { claimAchievement('passedSinceAccountCreated3'); + claimAchievement('passedSinceAccountCreated2'); + claimAchievement('passedSinceAccountCreated1'); + } else { + const createdAtTwoYearsLater = new Date($i.createdAt); + createdAtTwoYearsLater.setFullYear(createdAtTwoYearsLater.getFullYear() + 2); + if (now >= createdAtTwoYearsLater) { + claimAchievement('passedSinceAccountCreated2'); + claimAchievement('passedSinceAccountCreated1'); + } else { + const createdAtOneYearLater = new Date($i.createdAt); + createdAtOneYearLater.setFullYear(createdAtOneYearLater.getFullYear() + 1); + if (now >= createdAtOneYearLater) { + claimAchievement('passedSinceAccountCreated1'); + } + } } if (claimedAchievements.length >= 30) { @@ -229,7 +241,7 @@ export async function mainBoot() { const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt'); const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo'); - if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) { + if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) { if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) { popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed'); } diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue index d42146f941..96c9b9fd66 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -66,7 +66,7 @@ import * as os from '@/os.js'; import bytes from '@/filters/bytes.js'; import { hms } from '@/filters/hms.js'; import MkMediaRange from '@/components/MkMediaRange.vue'; -import { iAmModerator } from '@/account.js'; +import { $i, iAmModerator } from '@/account.js'; const props = defineProps<{ audio: Misskey.entities.DriveFile; @@ -96,8 +96,6 @@ function showMenu(ev: MouseEvent) { if (iAmModerator) { menu.push({ - type: 'divider', - }, { text: props.audio.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, icon: props.audio.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation', danger: true, @@ -105,6 +103,17 @@ function showMenu(ev: MouseEvent) { }); } + if ($i?.id === props.audio.userId) { + menu.push({ + type: 'divider', + }, { + type: 'link' as const, + text: i18n.ts._fileViewer.title, + icon: 'ti ti-info-circle', + to: `/my/drive/file/${props.audio.id}`, + }); + } + menuShowing.value = true; os.popupMenu(menu, ev.currentTarget ?? ev.target, { align: 'right', diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index 4ba2c76133..82f36fe5c4 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -59,7 +59,7 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -import { iAmModerator } from '@/account.js'; +import { $i, iAmModerator } from '@/account.js'; const props = withDefaults(defineProps<{ image: Misskey.entities.DriveFile; @@ -114,6 +114,13 @@ function showMenu(ev: MouseEvent) { action: () => { os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true }); }, + }] : []), ...($i?.id === props.image.userId ? [{ + type: 'divider' as const, + }, { + type: 'link' as const, + text: i18n.ts._fileViewer.title, + icon: 'ti ti-info-circle', + to: `/my/drive/file/${props.image.id}`, }] : [])], ev.currentTarget ?? ev.target); } diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index eab4fdfd6b..73c1b6ff9d 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -94,7 +94,7 @@ import * as os from '@/os.js'; import { isFullscreenNotSupported } from '@/scripts/device-kind.js'; import hasAudio from '@/scripts/media-has-audio.js'; import MkMediaRange from '@/components/MkMediaRange.vue'; -import { iAmModerator } from '@/account.js'; +import { $i, iAmModerator } from '@/account.js'; const props = defineProps<{ video: Misskey.entities.DriveFile; @@ -122,8 +122,6 @@ function showMenu(ev: MouseEvent) { if (iAmModerator) { menu.push({ - type: 'divider', - }, { text: props.video.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, icon: props.video.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation', danger: true, @@ -131,6 +129,17 @@ function showMenu(ev: MouseEvent) { }); } + if ($i?.id === props.video.userId) { + menu.push({ + type: 'divider', + }, { + type: 'link' as const, + text: i18n.ts._fileViewer.title, + icon: 'ti ti-info-circle', + to: `/my/drive/file/${props.video.id}`, + }); + } + menuShowing.value = true; os.popupMenu(menu, ev.currentTarget ?? ev.target, { align: 'right', diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 03a283cab3..5ca0eae012 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ appearNote.channel.name }} - + @@ -101,7 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only