From aea234f15ee864f594f42235c08b613873fae4a7 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 3 Dec 2023 19:24:45 +0900 Subject: [PATCH 01/11] fix: unnecessary logging in FanoutTimelineEndpointService --- packages/backend/src/core/FanoutTimelineEndpointService.ts | 4 ---- packages/backend/test/e2e/2fa.ts | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index 157fcbe877bc..c2dadcace446 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -70,7 +70,6 @@ export class FanoutTimelineEndpointService { const redisTimeline: MiNote[] = []; let readFromRedis = 0; let lastSuccessfulRate = 1; // rateをキャッシュする? - let trialCount = 1; while ((redisResultIds.length - readFromRedis) !== 0) { const remainingToRead = ps.limit - redisTimeline.length; @@ -85,8 +84,6 @@ export class FanoutTimelineEndpointService { redisTimeline.push(...gotFromDb); lastSuccessfulRate = gotFromDb.length / noteIds.length; - console.log(`fanoutTimelineTrial#${trialCount++}: req: ${ps.limit}, tried: ${noteIds.length}, got: ${gotFromDb.length}, rate: ${lastSuccessfulRate}, total: ${redisTimeline.length}, fromRedis: ${redisResultIds.length}`); - if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) { // 十分Redisからとれた return redisTimeline.slice(0, ps.limit); @@ -97,7 +94,6 @@ export class FanoutTimelineEndpointService { const remainingToRead = ps.limit - redisTimeline.length; const gotFromDb = await ps.dbFallback(noteIds[noteIds.length - 1], ps.sinceId, remainingToRead); redisTimeline.push(...gotFromDb); - console.log(`fanoutTimelineTrial#db: req: ${ps.limit}, tried: ${remainingToRead}, got: ${gotFromDb.length}, since: ${noteIds[noteIds.length - 1]}, until: ${ps.untilId}, total: ${redisTimeline.length}`); return redisTimeline; } diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index ed967d2620da..d2b186a71919 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -343,7 +343,7 @@ describe('2要素認証', () => { }), password: '', }); - assert.strictEqual(signinResponse2.status, 200); + assert.strictEqual(signinResponse2.status, 200, JSON.stringify(signinResponse2.body)); assert.notEqual(signinResponse2.body.i, undefined); // 後片付け From df9c03d8b808d6a174d64bc7a760310200f048a9 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 3 Dec 2023 20:09:05 +0900 Subject: [PATCH 02/11] chore: TimelineOptions --- .../src/core/FanoutTimelineEndpointService.ts | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index c2dadcace446..b98bb51a4384 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -13,6 +13,18 @@ import type { NotesRepository } from '@/models/_.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +type TimelineOptions = { + untilId: string | null, + sinceId: string | null, + limit: number, + allowPartial: boolean, + me?: { id: MiUser['id'] } | undefined | null, + useDbFallback: boolean, + redisTimelines: string[], + noteFilter: (note: MiNote) => boolean, + dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise, +}; + @Injectable() export class FanoutTimelineEndpointService { constructor( @@ -25,32 +37,12 @@ export class FanoutTimelineEndpointService { } @bindThis - async timeline(ps: { - untilId: string | null, - sinceId: string | null, - limit: number, - allowPartial: boolean, - me?: { id: MiUser['id'] } | undefined | null, - useDbFallback: boolean, - redisTimelines: string[], - noteFilter: (note: MiNote) => boolean, - dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise, - }): Promise[]> { + async timeline(ps: TimelineOptions): Promise[]> { return await this.noteEntityService.packMany(await this.getMiNotes(ps), ps.me); } @bindThis - private async getMiNotes(ps: { - untilId: string | null, - sinceId: string | null, - limit: number, - allowPartial: boolean, - me?: { id: MiUser['id'] } | undefined | null, - useDbFallback: boolean, - redisTimelines: string[], - noteFilter: (note: MiNote) => boolean, - dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise, - }): Promise { + private async getMiNotes(ps: TimelineOptions): Promise { let noteIds: string[]; let shouldFallbackToDb = false; From aa7bdd74d742fd6ce76e55cce53e64ddbb8d3846 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 3 Dec 2023 20:47:49 +0900 Subject: [PATCH 03/11] chore: add FanoutTimelineName type --- .../src/core/FanoutTimelineEndpointService.ts | 4 +-- .../backend/src/core/FanoutTimelineService.ts | 36 ++++++++++++++++--- .../api/endpoints/notes/hybrid-timeline.ts | 4 +-- .../api/endpoints/notes/user-list-timeline.ts | 2 -- .../src/server/api/endpoints/users/notes.ts | 3 +- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index b98bb51a4384..c41304980092 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -11,7 +11,7 @@ import type { MiNote } from '@/models/Note.js'; import { Packed } from '@/misc/json-schema.js'; import type { NotesRepository } from '@/models/_.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js'; type TimelineOptions = { untilId: string | null, @@ -20,7 +20,7 @@ type TimelineOptions = { allowPartial: boolean, me?: { id: MiUser['id'] } | undefined | null, useDbFallback: boolean, - redisTimelines: string[], + redisTimelines: FanoutTimelineName[], noteFilter: (note: MiNote) => boolean, dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise, }; diff --git a/packages/backend/src/core/FanoutTimelineService.ts b/packages/backend/src/core/FanoutTimelineService.ts index 6a1b0aa87929..654a035a5f2e 100644 --- a/packages/backend/src/core/FanoutTimelineService.ts +++ b/packages/backend/src/core/FanoutTimelineService.ts @@ -9,6 +9,34 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; +export type FanoutTimelineName = + // home timeline + | `homeTimeline:${string}` + | `homeTimelineWithFiles:${string}` // only notes with files are included + // local timeline + | `localTimeline` // replies are not included + | `localTimelineWithFiles` // only non-reply notes with files are included + | `localTimelineWithReplies` // only replies are included + + // antenna + | `antennaTimeline:${string}` + + // user timeline + | `userTimeline:${string}` // replies are not included + | `userTimelineWithFiles:${string}` // only non-reply notes with files are included + | `userTimelineWithReplies:${string}` // only replies are included + | `userTimelineWithChannel:${string}` // only channel notes are included, replies are included + + // user list timelines + | `userListTimeline:${string}` + | `userListTimelineWithFiles:${string}` // only notes with files are included + + // channel timelines + | `channelTimeline:${string}` // replies are included + + // role timelines + | `roleTimeline:${string}` // any notes are included + @Injectable() export class FanoutTimelineService { constructor( @@ -20,7 +48,7 @@ export class FanoutTimelineService { } @bindThis - public push(tl: string, id: string, maxlen: number, pipeline: Redis.ChainableCommander) { + public push(tl: FanoutTimelineName, id: string, maxlen: number, pipeline: Redis.ChainableCommander) { // リモートから遅れて届いた(もしくは後から追加された)投稿日時が古い投稿が追加されるとページネーション時に問題を引き起こすため、 // 3分以内に投稿されたものでない場合、Redisにある最古のIDより新しい場合のみ追加する if (this.idService.parse(id).date.getTime() > Date.now() - 1000 * 60 * 3) { @@ -41,7 +69,7 @@ export class FanoutTimelineService { } @bindThis - public get(name: string, untilId?: string | null, sinceId?: string | null) { + public get(name: FanoutTimelineName, untilId?: string | null, sinceId?: string | null) { if (untilId && sinceId) { return this.redisForTimelines.lrange('list:' + name, 0, -1) .then(ids => ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1)); @@ -58,7 +86,7 @@ export class FanoutTimelineService { } @bindThis - public getMulti(name: string[], untilId?: string | null, sinceId?: string | null): Promise { + public getMulti(name: FanoutTimelineName[], untilId?: string | null, sinceId?: string | null): Promise { const pipeline = this.redisForTimelines.pipeline(); for (const n of name) { pipeline.lrange('list:' + n, 0, -1); @@ -79,7 +107,7 @@ export class FanoutTimelineService { } @bindThis - public purge(name: string) { + public purge(name: FanoutTimelineName) { return this.redisForTimelines.del('list:' + name); } } diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index deb9e014c437..ba8712cd897a 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -14,7 +14,7 @@ import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { CacheService } from '@/core/CacheService.js'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MetaService } from '@/core/MetaService.js'; @@ -124,7 +124,7 @@ export default class extends Endpoint { // eslint- this.cacheService.userBlockedCache.fetch(me.id), ]); - let timelineConfig: string[]; + let timelineConfig: FanoutTimelineName[]; if (ps.withFiles) { timelineConfig = [ diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index f8f64738fee1..dd77f6133c8f 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -13,7 +13,6 @@ import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { MiLocalUser } from '@/models/User.js'; import { MetaService } from '@/core/MetaService.js'; @@ -84,7 +83,6 @@ export default class extends Endpoint { // eslint- private activeUsersChart: ActiveUsersChart, private cacheService: CacheService, private idService: IdService, - private fanoutTimelineService: FanoutTimelineService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, private metaService: MetaService, diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 4a358b39cb0e..890e25cb7fe5 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -16,6 +16,7 @@ import { QueryService } from '@/core/QueryService.js'; import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; export const meta = { tags: ['users', 'notes'], @@ -97,7 +98,7 @@ export default class extends Endpoint { // eslint- this.cacheService.userMutingsCache.fetch(me.id), ]) : [new Set()]; - const redisTimelines = [ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`]; + const redisTimelines: FanoutTimelineName[] = [ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`]; if (ps.withReplies) redisTimelines.push(`userTimelineWithReplies:${ps.userId}`); if (ps.withChannelNotes) redisTimelines.push(`userTimelineWithChannel:${ps.userId}`); From a402ba67330e962d8f33e932560adb08a38bf449 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 3 Dec 2023 21:17:29 +0900 Subject: [PATCH 04/11] chore: forbid specifying both withReplies and withFiles since it's not implemented correctly --- .../src/server/api/endpoints/notes/hybrid-timeline.ts | 8 ++++++++ .../src/server/api/endpoints/notes/local-timeline.ts | 8 ++++++++ packages/backend/src/server/api/endpoints/users/notes.ts | 9 +++++++++ 3 files changed, 25 insertions(+) diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index ba8712cd897a..0119131b9ef5 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -43,6 +43,12 @@ export const meta = { code: 'STL_DISABLED', id: '620763f4-f621-4533-ab33-0577a1a3c342', }, + + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f' + }, }, } as const; @@ -93,6 +99,8 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.stlDisabled); } + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + const serverSettings = await this.metaService.fetch(); if (!serverSettings.enableFanoutTimeline) { diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 97b05016ec3d..873896f3be70 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -39,6 +39,12 @@ export const meta = { code: 'LTL_DISABLED', id: '45a6eb02-7695-4393-b023-dd3be9aaaefd', }, + + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: 'dd9c8400-1cb5-4eef-8a31-200c5f933793' + }, }, } as const; @@ -82,6 +88,8 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.ltlDisabled); } + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + const serverSettings = await this.metaService.fetch(); if (!serverSettings.enableFanoutTimeline) { diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 890e25cb7fe5..2bfab6227b3b 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -17,6 +17,7 @@ import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; +import { ApiError } from '@/server/api/error.js'; export const meta = { tags: ['users', 'notes'], @@ -37,6 +38,12 @@ export const meta = { code: 'NO_SUCH_USER', id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b', }, + + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222', + }, }, } as const; @@ -78,6 +85,8 @@ export default class extends Endpoint { // eslint- const serverSettings = await this.metaService.fetch(); + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + if (!serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb({ untilId, From e81e5478c2ba3d4707b79457d3c83e7c4c8fcdcb Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 3 Dec 2023 21:45:03 +0900 Subject: [PATCH 05/11] chore: filter mutes, replies, renotes, files in FanoutTimelineEndpointService --- .../src/core/FanoutTimelineEndpointService.ts | 58 ++++++++++++++++++- packages/backend/src/misc/is-reply.ts | 10 ++++ .../server/api/endpoints/channels/timeline.ts | 16 +---- .../api/endpoints/notes/hybrid-timeline.ts | 28 +-------- .../api/endpoints/notes/local-timeline.ts | 33 ++--------- .../server/api/endpoints/notes/timeline.ts | 20 +------ .../api/endpoints/notes/user-list-timeline.ts | 34 +---------- .../src/server/api/endpoints/users/notes.ts | 22 ++----- 8 files changed, 83 insertions(+), 138 deletions(-) create mode 100644 packages/backend/src/misc/is-reply.ts diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index c41304980092..20f3ac360051 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -12,6 +12,11 @@ import { Packed } from '@/misc/json-schema.js'; import type { NotesRepository } from '@/models/_.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { isPureRenote } from '@/misc/is-pure-renote.js'; +import { CacheService } from '@/core/CacheService.js'; +import { isReply } from '@/misc/is-reply.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.js'; type TimelineOptions = { untilId: string | null, @@ -21,7 +26,12 @@ type TimelineOptions = { me?: { id: MiUser['id'] } | undefined | null, useDbFallback: boolean, redisTimelines: FanoutTimelineName[], - noteFilter: (note: MiNote) => boolean, + noteFilter?: (note: MiNote) => boolean, + alwaysIncludeMyNotes?: boolean; + ignoreAuthorFromMute?: boolean; + excludeNoFiles?: boolean; + excludeReplies?: boolean; + excludePureRenotes: boolean; dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise, }; @@ -32,6 +42,7 @@ export class FanoutTimelineEndpointService { private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, + private cacheService: CacheService, private fanoutTimelineService: FanoutTimelineService, ) { } @@ -59,6 +70,49 @@ export class FanoutTimelineEndpointService { shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0); if (!shouldFallbackToDb) { + let filter = ps.noteFilter ?? (_note => true); + + if (ps.alwaysIncludeMyNotes && ps.me) { + const me = ps.me; + const parentFilter = filter; + filter = (note) => note.userId === me.id || parentFilter(note); + } + + if (ps.excludeNoFiles) { + const parentFilter = filter; + filter = (note) => note.fileIds.length !== 0 && parentFilter(note); + } + + if (ps.excludeReplies) { + const parentFilter = filter; + filter = (note) => !isReply(note, ps.me) && parentFilter(note); + } + + if (ps.me) { + const me = ps.me; + const [ + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + userMutedInstances, + ] = await Promise.all([ + this.cacheService.userMutingsCache.fetch(ps.me.id), + this.cacheService.renoteMutingsCache.fetch(ps.me.id), + this.cacheService.userBlockedCache.fetch(ps.me.id), + this.cacheService.userProfileCache.fetch(me.id).then(p => new Set(p.mutedInstances)), + ]); + + const parentFilter = filter; + filter = (note) => { + if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromMute)) return false; + if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false; + if (isPureRenote(note) && isUserRelated(note, userIdsWhoMeMutingRenotes, ps.ignoreAuthorFromMute)) return false; + if (isInstanceMuted(note, userMutedInstances)) return false; + + return parentFilter(note); + }; + } + const redisTimeline: MiNote[] = []; let readFromRedis = 0; let lastSuccessfulRate = 1; // rateをキャッシュする? @@ -72,7 +126,7 @@ export class FanoutTimelineEndpointService { readFromRedis += noteIds.length; - const gotFromDb = await this.getAndFilterFromDb(noteIds, ps.noteFilter); + const gotFromDb = await this.getAndFilterFromDb(noteIds, filter); redisTimeline.push(...gotFromDb); lastSuccessfulRate = gotFromDb.length / noteIds.length; diff --git a/packages/backend/src/misc/is-reply.ts b/packages/backend/src/misc/is-reply.ts new file mode 100644 index 000000000000..f0525783b9fb --- /dev/null +++ b/packages/backend/src/misc/is-reply.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MiUser } from '@/models/User.js'; + +export function isReply(note: any, me?: { id: MiUser['id'] } | undefined | null): boolean { + return note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id); +} diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 9ef494d6d8e6..006228ceee61 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -4,15 +4,13 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository, MiNote, NotesRepository } from '@/models/_.js'; +import type { ChannelsRepository, NotesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { CacheService } from '@/core/CacheService.js'; import { MetaService } from '@/core/MetaService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; @@ -94,12 +92,6 @@ export default class extends Endpoint { // eslint- return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me); } - const [ - userIdsWhoMeMuting, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - ]) : [new Set()]; - return await this.fanoutTimelineEndpointService.timeline({ untilId, sinceId, @@ -108,11 +100,7 @@ export default class extends Endpoint { // eslint- me, useDbFallback: true, redisTimelines: [`channelTimeline:${channel.id}`], - noteFilter: note => { - if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; - - return true; - }, + excludePureRenotes: false, dbFallback: async (untilId, sinceId, limit) => { return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me); }, diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 0119131b9ef5..effcbaf2ee49 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -12,7 +12,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { CacheService } from '@/core/CacheService.js'; import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; @@ -122,16 +121,6 @@ export default class extends Endpoint { // eslint- return await this.noteEntityService.packMany(timeline, me); } - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - ] = await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]); - let timelineConfig: FanoutTimelineName[]; if (ps.withFiles) { @@ -160,21 +149,8 @@ export default class extends Endpoint { // eslint- me, redisTimelines: timelineConfig, useDbFallback: serverSettings.enableFanoutTimelineDbFallback, - noteFilter: (note) => { - if (note.userId === me.id) { - return true; - } - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } - - return true; - }, + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ untilId, sinceId, diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 873896f3be70..0aec6977aaf0 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -5,7 +5,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { MiNote, NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; @@ -13,7 +13,6 @@ import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { QueryService } from '@/core/QueryService.js'; import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; @@ -110,16 +109,6 @@ export default class extends Endpoint { // eslint- return await this.noteEntityService.packMany(timeline, me); } - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]) : [new Set(), new Set(), new Set()]; - const timeline = await this.fanoutTimelineEndpointService.timeline({ untilId, sinceId, @@ -127,23 +116,9 @@ export default class extends Endpoint { // eslint- allowPartial: ps.allowPartial, me, useDbFallback: serverSettings.enableFanoutTimelineDbFallback, - redisTimelines: ps.withFiles ? ['localTimelineWithFiles'] : ['localTimeline', 'localTimelineWithReplies'], - noteFilter: note => { - if (me && (note.userId === me.id)) { - return true; - } - if (!ps.withReplies && note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id)) return false; - if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } - - return true; - }, + redisTimelines: ps.withFiles ? ['localTimelineWithFiles'] : ps.withReplies ? ['localTimeline', 'localTimelineWithReplies'] : ['localTimeline'], + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ untilId, sinceId, diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 74d0a6e0c0de..790bcbe1512d 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -13,7 +13,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MiLocalUser } from '@/models/User.js'; import { MetaService } from '@/core/MetaService.js'; @@ -98,14 +97,8 @@ export default class extends Endpoint { // eslint- const [ followings, - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, ] = await Promise.all([ this.cacheService.userFollowingsCache.fetch(me.id), - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), ]); const timeline = this.fanoutTimelineEndpointService.timeline({ @@ -116,18 +109,9 @@ export default class extends Endpoint { // eslint- me, useDbFallback: serverSettings.enableFanoutTimelineDbFallback, redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`], + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, noteFilter: note => { - if (note.userId === me.id) { - return true; - } - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } if (note.reply && note.reply.visibility === 'followers') { if (!Object.hasOwn(followings, note.reply.userId)) return false; } diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index dd77f6133c8f..10d3a7a69796 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -5,19 +5,17 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; -import type { MiNote, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; +import type { MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { QueryService } from '@/core/QueryService.js'; import { MiLocalUser } from '@/models/User.js'; import { MetaService } from '@/core/MetaService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; -import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -119,18 +117,6 @@ export default class extends Endpoint { // eslint- await this.noteEntityService.packMany(timeline, me); } - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - userMutedInstances, - ] = await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - this.cacheService.userProfileCache.fetch(me.id).then(p => new Set(p.mutedInstances)), - ]); - const timeline = await this.fanoutTimelineEndpointService.timeline({ untilId, sinceId, @@ -139,22 +125,8 @@ export default class extends Endpoint { // eslint- me, useDbFallback: serverSettings.enableFanoutTimelineDbFallback, redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`], - noteFilter: note => { - if (note.userId === me.id) { - return true; - } - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } - if (isInstanceMuted(note, userMutedInstances)) return false; - - return true; - }, + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb(list, { untilId, sinceId, diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 2bfab6227b3b..b32128a8aa7d 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -11,7 +11,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { QueryService } from '@/core/QueryService.js'; import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; @@ -101,12 +100,6 @@ export default class extends Endpoint { // eslint- return await this.noteEntityService.packMany(timeline, me); } - const [ - userIdsWhoMeMuting, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - ]) : [new Set()]; - const redisTimelines: FanoutTimelineName[] = [ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`]; if (ps.withReplies) redisTimelines.push(`userTimelineWithReplies:${ps.userId}`); @@ -122,18 +115,11 @@ export default class extends Endpoint { // eslint- me, redisTimelines, useDbFallback: true, + ignoreAuthorFromMute: true, + excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies + excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files + excludePureRenotes: !ps.withRenotes, noteFilter: note => { - if (ps.withFiles && note.fileIds.length === 0) { - return false; - } - if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false; - - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (ps.withRenotes === false) return false; - } - } - if (note.channel?.isSensitive && !isSelf) return false; if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false; if (note.visibility === 'followers' && !isFollowing && !isSelf) return false; From ccb3ba54c489bde1b27d20ad14aef63f13f032b5 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 3 Dec 2023 21:49:33 +0900 Subject: [PATCH 06/11] revert unintended changes --- packages/backend/test/e2e/2fa.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index d2b186a71919..ed967d2620da 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -343,7 +343,7 @@ describe('2要素認証', () => { }), password: '', }); - assert.strictEqual(signinResponse2.status, 200, JSON.stringify(signinResponse2.body)); + assert.strictEqual(signinResponse2.status, 200); assert.notEqual(signinResponse2.body.i, undefined); // 後片付け From 78ca700ed4e9091ccbd42a5d31a58ef15a37f4da Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 3 Dec 2023 21:59:58 +0900 Subject: [PATCH 07/11] use isReply in NoteCreateService --- packages/backend/src/core/FanoutTimelineEndpointService.ts | 2 +- packages/backend/src/core/NoteCreateService.ts | 7 ++++--- packages/backend/src/misc/is-reply.ts | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index 20f3ac360051..309d1610ad41 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -85,7 +85,7 @@ export class FanoutTimelineEndpointService { if (ps.excludeReplies) { const parentFilter = filter; - filter = (note) => !isReply(note, ps.me) && parentFilter(note); + filter = (note) => !isReply(note, ps.me?.id) && parentFilter(note); } if (ps.me) { diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index fd87edc28ee1..0110ebaf5e9a 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -57,6 +57,7 @@ import { FeaturedService } from '@/core/FeaturedService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { isReply } from '@/misc/is-reply.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -891,7 +892,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (note.visibility === 'specified' && !note.visibleUserIds.some(v => v === following.followerId)) continue; // 「自分自身への返信 or そのフォロワーへの返信」のどちらでもない場合 - if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === following.followerId)) { + if (isReply(note, following.followerId)) { if (!following.withReplies) continue; } @@ -909,7 +910,7 @@ export class NoteCreateService implements OnApplicationShutdown { ) continue; // 「自分自身への返信 or そのリストの作成者への返信」のどちらでもない場合 - if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === userListMembership.userListUserId)) { + if (isReply(note, userListMembership.userListUserId)) { if (!userListMembership.withReplies) continue; } @@ -927,7 +928,7 @@ export class NoteCreateService implements OnApplicationShutdown { } // 自分自身以外への返信 - if (note.replyId && note.replyUserId !== note.userId) { + if (isReply(note)) { this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.visibility === 'public' && note.userHost == null) { diff --git a/packages/backend/src/misc/is-reply.ts b/packages/backend/src/misc/is-reply.ts index f0525783b9fb..964c2aa15393 100644 --- a/packages/backend/src/misc/is-reply.ts +++ b/packages/backend/src/misc/is-reply.ts @@ -5,6 +5,6 @@ import { MiUser } from '@/models/User.js'; -export function isReply(note: any, me?: { id: MiUser['id'] } | undefined | null): boolean { - return note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id); +export function isReply(note: any, viewerId?: MiUser['id'] | undefined | null): boolean { + return note.replyId && note.replyUserId !== note.userId && note.replyUserId !== viewerId; } From 2b8e1edc15e21992d2b99c9c5091d8ef8d07b874 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 3 Dec 2023 22:19:42 +0900 Subject: [PATCH 08/11] fix: excludePureRenotes is not implemented --- packages/backend/src/core/FanoutTimelineEndpointService.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index 309d1610ad41..6775f0051ab4 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -88,6 +88,11 @@ export class FanoutTimelineEndpointService { filter = (note) => !isReply(note, ps.me?.id) && parentFilter(note); } + if (ps.excludePureRenotes) { + const parentFilter = filter; + filter = (note) => !isPureRenote(note) && parentFilter(note); + } + if (ps.me) { const me = ps.me; const [ From 57c48f5eb5fe79b31ca861ee9b438ce699473677 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 3 Dec 2023 22:25:18 +0900 Subject: [PATCH 09/11] fix: replies to me is excluded from local timeline --- .../backend/src/server/api/endpoints/notes/local-timeline.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 0aec6977aaf0..e8ba39bbf0eb 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -116,8 +116,9 @@ export default class extends Endpoint { // eslint- allowPartial: ps.allowPartial, me, useDbFallback: serverSettings.enableFanoutTimelineDbFallback, - redisTimelines: ps.withFiles ? ['localTimelineWithFiles'] : ps.withReplies ? ['localTimeline', 'localTimelineWithReplies'] : ['localTimeline'], + redisTimelines: ps.withFiles ? ['localTimelineWithFiles'] : ['localTimeline', 'localTimelineWithReplies'], alwaysIncludeMyNotes: true, + excludeReplies: !ps.withReplies, excludePureRenotes: !ps.withRenotes, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ untilId, From 1fb7952b749376f97443a2d739d431102246966f Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 3 Dec 2023 22:44:45 +0900 Subject: [PATCH 10/11] chore(frontend): forbid enabling both withReplies and withFiles --- packages/frontend/src/components/MkMenu.vue | 2 +- packages/frontend/src/pages/timeline.vue | 4 ++-- packages/frontend/src/types/menu.ts | 2 +- packages/frontend/src/ui/deck/tl-column.vue | 2 ++ 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index 9457bf385fb7..4fafd35f72c2 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -202,7 +202,7 @@ function focusDown() { } function switchItem(item: MenuSwitch & { ref: any }) { - if (item.disabled) return; + if (typeof item.disabled === 'boolean' ? item.disabled : item.disabled.value) return; item.ref = !item.ref; } diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index cfe270aefba0..b390d1931ddc 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -157,17 +157,17 @@ const headerActions = $computed(() => { os.popupMenu([{ type: 'switch', text: i18n.ts.showRenotes, - icon: 'ti ti-repeat', ref: $$(withRenotes), }, src === 'local' || src === 'social' ? { type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, ref: $$(withReplies), + disabled: $$(onlyFiles), } : undefined, { type: 'switch', text: i18n.ts.fileAttachedOnly, - icon: 'ti ti-photo', ref: $$(onlyFiles), + disabled: src === 'local' || src === 'social' ? $$(withReplies) : false, }], ev.currentTarget ?? ev.target); }, }, diff --git a/packages/frontend/src/types/menu.ts b/packages/frontend/src/types/menu.ts index 66061fcd70ed..fbe627176bfa 100644 --- a/packages/frontend/src/types/menu.ts +++ b/packages/frontend/src/types/menu.ts @@ -14,7 +14,7 @@ export type MenuLabel = { type: 'label', text: string }; export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, 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: string, disabled?: boolean }; +export type MenuSwitch = { type: 'switch', ref: Ref, text: string, disabled?: boolean | Ref }; export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction }; export type MenuParent = { type: 'parent', text: string, icon?: string, children: MenuItem[] | (() => Promise | MenuItem[]) }; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 9f24ea31ed8b..41582bbfe3d9 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -120,10 +120,12 @@ const menu = [{ type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, ref: $$(withReplies), + disabled: $$(onlyFiles), } : undefined, { type: 'switch', text: i18n.ts.fileAttachedOnly, ref: $$(onlyFiles), + disabled: props.column.tl === 'local' || props.column.tl === 'social' ? $$(withReplies) : false, }]; From 47112d68100723134c8219ccbed854615700ecb1 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sun, 3 Dec 2023 22:47:55 +0900 Subject: [PATCH 11/11] =?UTF-8?q?docs(changelog):=20=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=82=B9=E3=82=BF=E3=83=B3=E3=82=B9=E3=83=9F=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=8C=E5=8A=B9=E3=81=8B=E3=81=AA=E3=81=84=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E3=81=AE=E4=BF=AE=E6=AD=A3=E3=81=AB=E3=81=A4=E3=81=84?= =?UTF-8?q?=E3=81=A6=E8=A8=80=E5=8F=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 694f33012065..84e8e0d2512d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,9 @@ - Fix: 特定の条件下でチャンネルやユーザーのノート一覧に最新のノートが表示されなくなる問題を修正 - Fix: 何もノートしていないユーザーのフィードにアクセスするとエラーになる問題を修正 - Fix: リストタイムラインにてミュートが機能しないケースがある問題と、チャンネル投稿がストリーミングで流れてきてしまう問題を修正 #10443 +- Fix: Social/Local/Home Timelineにてインスタンスミュートが効かない問題 +- Fix: ユーザのノート一覧にてインスタンスミュートが効かない問題 +- Fix: チャンネルのノート一覧にてインスタンスミュートが効かない問題 ## 2023.11.1