From 7d0f995c9b176ae325a9ff9a4b239d93d63fc1a7 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 9 Jun 2025 20:44:29 -0400 Subject: [PATCH] hide muted threads from timelines --- packages/backend/src/core/CacheService.ts | 20 +++++++- .../backend/src/core/NoteCreateService.ts | 51 ++++++++++--------- packages/backend/src/core/NoteEditService.ts | 13 ++--- packages/backend/src/core/ReactionService.ts | 8 +-- .../src/core/entities/NoteEntityService.ts | 1 + .../backend/src/models/json-schema/note.ts | 6 +++ .../server/api/endpoints/channels/timeline.ts | 13 ++++- .../api/endpoints/notes/bubble-timeline.ts | 1 + .../api/endpoints/notes/global-timeline.ts | 3 ++ .../api/endpoints/notes/hybrid-timeline.ts | 7 +++ .../api/endpoints/notes/local-timeline.ts | 12 +++++ .../src/server/api/endpoints/notes/state.ts | 18 +++---- .../endpoints/notes/thread-muting/create.ts | 6 +++ .../endpoints/notes/thread-muting/delete.ts | 4 ++ .../server/api/endpoints/notes/timeline.ts | 5 ++ .../src/server/api/stream/Connection.ts | 5 +- .../backend/src/server/api/stream/channel.ts | 7 +++ packages/misskey-js/src/autogen/types.ts | 5 ++ 18 files changed, 134 insertions(+), 51 deletions(-) diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 2d37cd6bab..397d52d10e 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { In, IsNull } from 'typeorm'; -import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiNote, MiFollowing } from '@/models/_.js'; +import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiNote, MiFollowing, NoteThreadMutingsRepository } from '@/models/_.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; import { QuantumKVCache } from '@/misc/QuantumKVCache.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; @@ -46,6 +46,7 @@ export class CacheService implements OnApplicationShutdown { public userBlockingCache: QuantumKVCache>; public userBlockedCache: QuantumKVCache>; // NOTE: 「被」Blockキャッシュ public renoteMutingsCache: QuantumKVCache>; + public threadMutingsCache: QuantumKVCache>; public userFollowingsCache: QuantumKVCache>>; public userFollowersCache: QuantumKVCache>>; public hibernatedUserCache: QuantumKVCache; @@ -77,6 +78,9 @@ export class CacheService implements OnApplicationShutdown { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + @Inject(DI.noteThreadMutingsRepository) + private readonly noteThreadMutingsRepository: NoteThreadMutingsRepository, + private userEntityService: UserEntityService, private readonly internalEventService: InternalEventService, ) { @@ -145,6 +149,20 @@ export class CacheService implements OnApplicationShutdown { .then(ms => ms.map(m => [m.muterId, new Set(m.muteeIds)])), }); + this.threadMutingsCache = new QuantumKVCache>(this.internalEventService, 'threadMutings', { + lifetime: 1000 * 60 * 30, // 30m + fetcher: muterId => this.noteThreadMutingsRepository + .find({ where: { userId: muterId }, select: { threadId: true } }) + .then(ms => new Set(ms.map(m => m.threadId))), + bulkFetcher: muterIds => this.noteThreadMutingsRepository + .createQueryBuilder('muting') + .select('"muting"."userId"', 'userId') + .addSelect('array_agg("muting"."threadId")', 'threadIds') + .where({ userId: In(muterIds) }) + .getRawMany<{ userId: string, threadIds: string[] }>() + .then(ms => ms.map(m => [m.userId, new Set(m.threadIds)])), + }); + this.userFollowingsCache = new QuantumKVCache>>(this.internalEventService, 'userFollowings', { lifetime: 1000 * 60 * 30, // 30m fetcher: (key) => this.followingsRepository.findBy({ followerId: key }).then(xs => new Map(xs.map(f => [f.followeeId, f]))), diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index f4159facc3..bf7d209fef 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -676,18 +676,15 @@ export class NoteCreateService implements OnApplicationShutdown { }); // 通知 if (data.reply.userHost === null) { - const isThreadMuted = await this.noteThreadMutingsRepository.exists({ - where: { - userId: data.reply.userId, - threadId: data.reply.threadId ?? data.reply.id, - }, - }); + const threadId = data.reply.threadId ?? data.reply.id; const [ + isThreadMuted, userIdsWhoMeMuting, - ] = data.reply.userId ? await Promise.all([ + ] = await Promise.all([ + this.cacheService.threadMutingsCache.fetch(data.reply.userId).then(ms => ms.has(threadId)), this.cacheService.userMutingsCache.fetch(data.reply.userId), - ]) : [new Set()]; + ]); const muted = isUserRelated(note, userIdsWhoMeMuting); @@ -705,14 +702,17 @@ export class NoteCreateService implements OnApplicationShutdown { // Notify if (data.renote.userHost === null) { - const isThreadMuted = await this.noteThreadMutingsRepository.exists({ - where: { - userId: data.renote.userId, - threadId: data.renote.threadId ?? data.renote.id, - }, - }); + const threadId = data.renote.threadId ?? data.renote.id; - const muted = data.renote.userId && isUserRelated(note, await this.cacheService.userMutingsCache.fetch(data.renote.userId)); + const [ + isThreadMuted, + userIdsWhoMeMuting, + ] = await Promise.all([ + this.cacheService.threadMutingsCache.fetch(data.renote.userId).then(ms => ms.has(threadId)), + this.cacheService.userMutingsCache.fetch(data.renote.userId), + ]); + + const muted = data.renote.userId && isUserRelated(note, userIdsWhoMeMuting); if (!isThreadMuted && !muted) { nm.push(data.renote.userId, type); @@ -842,18 +842,23 @@ export class NoteCreateService implements OnApplicationShutdown { @bindThis private async createMentionedEvents(mentionedUsers: MinimumUser[], note: MiNote, nm: NotificationManager) { + const [ + threadMutings, + userMutings, + ] = await Promise.all([ + this.cacheService.threadMutingsCache.fetchMany(mentionedUsers.map(u => u.id)).then(ms => new Map(ms)), + this.cacheService.userMutingsCache.fetchMany(mentionedUsers.map(u => u.id)).then(ms => new Map(ms)), + ]); + // Only create mention events for local users, and users for whom the note is visible for (const u of mentionedUsers.filter(u => (note.visibility !== 'specified' || note.visibleUserIds.some(x => x === u.id)) && this.userEntityService.isLocalUser(u))) { - const isThreadMuted = await this.noteThreadMutingsRepository.exists({ - where: { - userId: u.id, - threadId: note.threadId ?? note.id, - }, - }); + const threadId = note.threadId ?? note.id; + const isThreadMuted = threadMutings.get(u.id)?.has(threadId); - const muted = u.id && isUserRelated(note, await this.cacheService.userMutingsCache.fetch(u.id)); + const mutings = userMutings.get(u.id); + const isUserMuted = mutings != null && isUserRelated(note, mutings); - if (isThreadMuted || muted) { + if (isThreadMuted || isUserMuted) { continue; } diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index 4be097465d..5b0d1980c3 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -647,18 +647,15 @@ export class NoteEditService implements OnApplicationShutdown { if (data.reply) { // 通知 if (data.reply.userHost === null) { - const isThreadMuted = await this.noteThreadMutingsRepository.exists({ - where: { - userId: data.reply.userId, - threadId: data.reply.threadId ?? data.reply.id, - }, - }); + const threadId = data.reply.threadId ?? data.reply.id; const [ + isThreadMuted, userIdsWhoMeMuting, - ] = data.reply.userId ? await Promise.all([ + ] = await Promise.all([ + this.cacheService.threadMutingsCache.fetch(data.reply.userId).then(ms => ms.has(threadId)), this.cacheService.userMutingsCache.fetch(data.reply.userId), - ]) : [new Set()]; + ]); const muted = isUserRelated(note, userIdsWhoMeMuting); diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 8d2dc7d4e8..bb56c6c745 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -275,12 +275,8 @@ export class ReactionService { // リアクションされたユーザーがローカルユーザーなら通知を作成 if (note.userHost === null) { - const isThreadMuted = await this.noteThreadMutingsRepository.exists({ - where: { - userId: note.userId, - threadId: note.threadId ?? note.id, - }, - }); + const threadId = note.threadId ?? note.id; + const isThreadMuted = await this.cacheService.threadMutingsCache.fetch(note.userId).then(ms => ms.has(threadId)); if (!isThreadMuted) { this.notificationService.createNotification(note.userId, 'reaction', { diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 4248fde77f..1875f8310b 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -462,6 +462,7 @@ export class NoteEntityService implements OnModuleInit { const packed: Packed<'Note'> = await awaitAll({ id: note.id, + threadId: note.threadId ?? note.id, createdAt: this.idService.parse(note.id).date.toISOString(), updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined, userId: note.userId, diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index b19c8f7c06..036bf1fc06 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -12,6 +12,12 @@ export const packedNoteSchema = { format: 'id', example: 'xxxxxxxxxx', }, + threadId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, createdAt: { type: 'string', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index fa5b948eca..2c8338c115 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -13,8 +13,8 @@ import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { MiLocalUser } from '@/models/User.js'; +import { CacheService } from '@/core/CacheService.js'; import { ApiError } from '../../error.js'; -import { Brackets } from 'typeorm'; export const meta = { tags: ['notes', 'channels'], @@ -83,6 +83,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private activeUsersChart: ActiveUsersChart, + private readonly cacheService: CacheService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -106,6 +107,8 @@ export default class extends Endpoint { // eslint- return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me), me); } + const threadMutings = me ? await this.cacheService.threadMutingsCache.fetch(me.id) : null; + return await this.fanoutTimelineEndpointService.timeline({ untilId, sinceId, @@ -119,6 +122,13 @@ export default class extends Endpoint { // eslint- dbFallback: async (untilId, sinceId, limit) => { return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me); }, + noteFilter: note => { + if (threadMutings?.has(note.threadId ?? note.id)) { + return false; + } + + return true; + }, }); }); } @@ -148,6 +158,7 @@ export default class extends Endpoint { // eslint- if (me) { this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateMutedNoteThreadQuery(query, me); } if (ps.withFiles) { diff --git a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts index 84d6aa0dc7..6bb5856f96 100644 --- a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts @@ -100,6 +100,7 @@ export default class extends Endpoint { // eslint- if (me) { this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateMutedNoteThreadQuery(query, me); } if (ps.withFiles) { diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 506ea6fcda..ce70c5ae5e 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -12,6 +12,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; +import { CacheService } from '@/core/CacheService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -67,6 +68,7 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, + private readonly cacheService: CacheService, ) { super(meta, paramDef, async (ps, me) => { const policies = await this.roleService.getUserPolicies(me ? me.id : null); @@ -90,6 +92,7 @@ export default class extends Endpoint { // eslint- if (me) { this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateMutedNoteThreadQuery(query, me); } if (ps.withFiles) { 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 a5623d1f03..78faa5bed9 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -147,8 +147,10 @@ export default class extends Endpoint { // eslint- const [ followings, + mutedThreads, ] = await Promise.all([ this.cacheService.userFollowingsCache.fetch(me.id), + this.cacheService.threadMutingsCache.fetch(me.id), ]); const redisTimeline = await this.fanoutTimelineEndpointService.timeline({ @@ -167,6 +169,10 @@ export default class extends Endpoint { // eslint- if (!followings.has(note.reply.userId) && note.reply.userId !== me.id) return false; } + if (mutedThreads.has(note.threadId ?? note.id)) { + return false; + } + return true; }, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ @@ -230,6 +236,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateSilencedUserQueryForNotes(query, me); this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateMutedNoteThreadQuery(query, me); if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); 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 41b1ee1086..a754e9720a 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -15,6 +15,7 @@ import { IdService } from '@/core/IdService.js'; import { QueryService } from '@/core/QueryService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { CacheService } from '@/core/CacheService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -83,6 +84,7 @@ export default class extends Endpoint { // eslint- private idService: IdService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, + private readonly cacheService: CacheService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -115,6 +117,8 @@ export default class extends Endpoint { // eslint- return await this.noteEntityService.packMany(timeline, me); } + const mutedThreads = me ? await this.cacheService.threadMutingsCache.fetch(me.id) : null; + const timeline = await this.fanoutTimelineEndpointService.timeline({ untilId, sinceId, @@ -139,6 +143,13 @@ export default class extends Endpoint { // eslint- withBots: ps.withBots, withRenotes: ps.withRenotes, }, me), + noteFilter: note => { + if (mutedThreads?.has(note.threadId ?? note.id)) { + return false; + } + + return true; + }, }); if (me) { @@ -185,6 +196,7 @@ export default class extends Endpoint { // eslint- if (me) { this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateMutedNoteThreadQuery(query, me); } if (ps.withFiles) { diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index 448e704528..985b7e004e 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { NotesRepository, NoteThreadMutingsRepository, NoteFavoritesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; +import { CacheService } from '@/core/CacheService.js'; export const meta = { tags: ['notes'], @@ -55,30 +56,25 @@ export default class extends Endpoint { // eslint- @Inject(DI.noteFavoritesRepository) private noteFavoritesRepository: NoteFavoritesRepository, + + private readonly cacheService: CacheService, ) { super(meta, paramDef, async (ps, me) => { const note = await this.notesRepository.findOneByOrFail({ id: ps.noteId }); const [favorite, threadMuting] = await Promise.all([ - this.noteFavoritesRepository.count({ + this.noteFavoritesRepository.exists({ where: { userId: me.id, noteId: note.id, }, - take: 1, - }), - this.noteThreadMutingsRepository.count({ - where: { - userId: me.id, - threadId: note.threadId ?? note.id, - }, - take: 1, }), + this.cacheService.threadMutingsCache.fetch(me.id).then(ms => ms.has(note.threadId ?? note.id)), ]); return { - isFavorited: favorite !== 0, - isMutedThread: threadMuting !== 0, + isFavorited: favorite, + isMutedThread: threadMuting, }; }); } diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts index 29c6aa7434..94d48c3f72 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts @@ -10,6 +10,7 @@ import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; +import { CacheService } from '@/core/CacheService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -52,6 +53,7 @@ export default class extends Endpoint { // eslint- private getterService: GetterService, private idService: IdService, + private readonly cacheService: CacheService, ) { super(meta, paramDef, async (ps, me) => { const note = await this.getterService.getNote(ps.noteId).catch(err => { @@ -59,6 +61,7 @@ export default class extends Endpoint { // eslint- throw err; }); + /* const mutedNotes = await this.notesRepository.find({ where: [{ id: note.threadId ?? note.id, @@ -66,12 +69,15 @@ export default class extends Endpoint { // eslint- threadId: note.threadId ?? note.id, }], }); + */ await this.noteThreadMutingsRepository.insert({ id: this.idService.gen(), threadId: note.threadId ?? note.id, userId: me.id, }); + + await this.cacheService.threadMutingsCache.refresh(me.id); }); } } diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts index 50ce4fb89a..2e031c579a 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts @@ -8,6 +8,7 @@ import type { NoteThreadMutingsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; +import { CacheService } from '@/core/CacheService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -47,6 +48,7 @@ export default class extends Endpoint { // eslint- private noteThreadMutingsRepository: NoteThreadMutingsRepository, private getterService: GetterService, + private readonly cacheService: CacheService, ) { super(meta, paramDef, async (ps, me) => { const note = await this.getterService.getNote(ps.noteId).catch(err => { @@ -58,6 +60,8 @@ export default class extends Endpoint { // eslint- threadId: note.threadId ?? note.id, userId: me.id, }); + + await this.cacheService.threadMutingsCache.refresh(me.id); }); } } diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 44c539eaad..10f22155e0 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -99,8 +99,10 @@ export default class extends Endpoint { // eslint- const [ followings, + threadMutings, ] = await Promise.all([ this.cacheService.userFollowingsCache.fetch(me.id), + this.cacheService.threadMutingsCache.fetch(me.id), ]); const timeline = this.fanoutTimelineEndpointService.timeline({ @@ -119,6 +121,8 @@ export default class extends Endpoint { // eslint- } if (!ps.withBots && note.user?.isBot) return false; + if (threadMutings.has(note.threadId ?? note.id)) return false; + return true; }, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ @@ -167,6 +171,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateSilencedUserQueryForNotes(query, me); this.queryService.generateMutedUserQueryForNotes(query, me); this.queryService.generateBlockedUserQueryForNotes(query, me); + this.queryService.generateMutedNoteThreadQuery(query, me); if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts index 0ee7078eb2..8ca473ff56 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -42,6 +42,7 @@ export default class Connection { public userIdsWhoBlockingMe: Set = new Set(); public userIdsWhoMeMutingRenotes: Set = new Set(); public userMutedInstances: Set = new Set(); + public userMutedThreads: Set = new Set(); private fetchIntervalId: NodeJS.Timeout | null = null; private closingConnection = false; private logger: Logger; @@ -67,13 +68,14 @@ export default class Connection { @bindThis public async fetch() { if (this.user == null) return; - const [userProfile, following, followingChannels, userIdsWhoMeMuting, userIdsWhoBlockingMe, userIdsWhoMeMutingRenotes] = await Promise.all([ + const [userProfile, following, followingChannels, userIdsWhoMeMuting, userIdsWhoBlockingMe, userIdsWhoMeMutingRenotes, threadMutings] = await Promise.all([ this.cacheService.userProfileCache.fetch(this.user.id), this.cacheService.userFollowingsCache.fetch(this.user.id), this.channelFollowingService.userFollowingChannelsCache.fetch(this.user.id), this.cacheService.userMutingsCache.fetch(this.user.id), this.cacheService.userBlockedCache.fetch(this.user.id), this.cacheService.renoteMutingsCache.fetch(this.user.id), + this.cacheService.threadMutingsCache.fetch(this.user.id), ]); this.userProfile = userProfile; this.following = following; @@ -82,6 +84,7 @@ export default class Connection { this.userIdsWhoBlockingMe = userIdsWhoBlockingMe; this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes; this.userMutedInstances = new Set(userProfile.mutedInstances); + this.userMutedThreads = threadMutings; } @bindThis diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 40ad454adb..397f9e9367 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -53,6 +53,10 @@ export default abstract class Channel { return this.connection.userMutedInstances; } + protected get userMutedThreads() { + return this.connection.userMutedThreads; + } + protected get followingChannels() { return this.connection.followingChannels; } @@ -94,6 +98,9 @@ export default abstract class Channel { // 流れてきたNoteがリノートをミュートしてるユーザが行ったもの if (isRenotePacked(note) && !isQuotePacked(note) && this.userIdsWhoMeMutingRenotes.has(note.user.id)) return true; + // Muted thread + if (this.userMutedThreads.has(note.threadId)) return true; + // If it's a boost (pure renote) then we need to check the target as well if (isPackedPureRenote(note) && note.renote && this.isNoteMutedOrBlocked(note.renote)) return true; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 97356e0c6e..e019df95ac 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4652,6 +4652,11 @@ export type components = { * @example xxxxxxxxxx */ id: string; + /** + * Format: id + * @example xxxxxxxxxx + */ + threadId: string; /** Format: date-time */ createdAt: string; /** Format: date-time */