diff options
| author | おさむのひと <46447427+samunohito@users.noreply.github.com> | 2025-11-07 08:39:21 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-07 08:39:21 +0900 |
| commit | 729abbef621aea5b8b697644181915935b74bbf8 (patch) | |
| tree | 27545c0cfd3e6272dd40de2c77daf0d2adec3e6c /packages/backend/src/server/api/endpoints/notes/timeline.ts | |
| parent | Bump version to 2025.11.0-alpha.1 (diff) | |
| download | misskey-729abbef621aea5b8b697644181915935b74bbf8.tar.gz misskey-729abbef621aea5b8b697644181915935b74bbf8.tar.bz2 misskey-729abbef621aea5b8b697644181915935b74bbf8.zip | |
feat: チャンネルミュートの実装 (#14105)
* add channel_muting table and entities
* add channel_muting services
* タイムライン取得処理への組み込み
* misskey-jsの型とインターフェース生成
* Channelスキーマにミュート情報を追加
* フロントエンドの実装
* 条件が逆だったのを修正
* 期限切れミュートを掃除する機能を実装
* TLの抽出条件調節
* 名前の変更と変更不要の差分をロールバック
* 修正漏れ
* isChannelRelatedの条件に誤りがあった
* [wip] テスト追加
* テストの追加と検出した不備の修正
* fix test
* fix CHANGELOG.md
* 通常はFTTにしておく
* 実装忘れ対応
* fix merge
* fix merge
* add channel tl test
* fix CHANGELOG.md
* remove unused import
* fix lint
* fix test
* fix favorite -> favorited
* exclude -> include
* fix CHANGELOG.md
* fix CHANGELOG.md
* maintenance
* fix CHANGELOG.md
* fix
* fix ci
* regenerate
* fix
* Revert "fix"
This reverts commit 699d50c6ec798777d8e9667cb5d45a26b06bfc93.
* fixed
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Diffstat (limited to 'packages/backend/src/server/api/endpoints/notes/timeline.ts')
| -rw-r--r-- | packages/backend/src/server/api/endpoints/notes/timeline.ts | 52 |
1 files changed, 32 insertions, 20 deletions
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index eeeb797efc..fe9c412be4 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -5,7 +5,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js'; +import type { NotesRepository, MiMeta } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; @@ -16,6 +16,8 @@ import { CacheService } from '@/core/CacheService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { ChannelMutingService } from '@/core/ChannelMutingService.js'; +import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; export const meta = { tags: ['notes'], @@ -61,15 +63,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.notesRepository) private notesRepository: NotesRepository, - @Inject(DI.channelFollowingsRepository) - private channelFollowingsRepository: ChannelFollowingsRepository, - private noteEntityService: NoteEntityService, private activeUsersChart: ActiveUsersChart, private idService: IdService, private cacheService: CacheService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private userFollowingService: UserFollowingService, + private channelMutingService: ChannelMutingService, + private channelFollowingService: ChannelFollowingService, private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { @@ -140,11 +141,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; includeMyRenotes: boolean; includeRenotedMyNotes: boolean; includeLocalRenotes: boolean; withFiles: boolean; withRenotes: boolean; }, me: MiLocalUser) { const followees = await this.userFollowingService.getFollowees(me.id); - const followingChannels = await this.channelFollowingsRepository.find({ - where: { - followerId: me.id, - }, - }); + + const mutingChannelIds = await this.channelMutingService + .list({ requestUserId: me.id }, { idOnly: true }) + .then(x => x.map(x => x.id)); + const followingChannelIds = await this.channelFollowingService + .list({ requestUserId: me.id }, { idOnly: true }) + .then(x => x.map(x => x.id).filter(x => !mutingChannelIds.includes(x))); //#region Construct query const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) @@ -154,15 +157,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - if (followees.length > 0 && followingChannels.length > 0) { + if (followees.length > 0 && followingChannelIds.length > 0) { // ユーザー・チャンネルともにフォローあり const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; - const followingChannelIds = followingChannels.map(x => x.followeeId); query.andWhere(new Brackets(qb => { qb .where(new Brackets(qb2 => { qb2 - .where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }) + .andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }) .andWhere('note.channelId IS NULL'); })) .orWhere('note.channelId IN (:...followingChannelIds)', { followingChannelIds }); @@ -170,22 +172,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } else if (followees.length > 0) { // ユーザーフォローのみ(チャンネルフォローなし) const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; - query - .andWhere('note.channelId IS NULL') - .andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); - } else if (followingChannels.length > 0) { + query.andWhere(new Brackets(qb => { + qb + .andWhere('note.channelId IS NULL') + .andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); + if (mutingChannelIds.length > 0) { + qb.andWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + } + })); + } else if (followingChannelIds.length > 0) { // チャンネルフォローのみ(ユーザーフォローなし) - const followingChannelIds = followingChannels.map(x => x.followeeId); query.andWhere(new Brackets(qb => { qb + // renoteChannelIdは見る必要が無い + // ・HTLに流れてくるチャンネル=フォローしているチャンネル + // ・HTLにフォロー外のチャンネルが流れるのは、フォローしているユーザがそのチャンネル投稿をリノートした場合のみ + // つまり、ユーザフォローしてない前提のこのブロックでは見る必要が無い .where('note.channelId IN (:...followingChannelIds)', { followingChannelIds }) .orWhere('note.userId = :meId', { meId: me.id }); })); } else { // フォローなし - query - .andWhere('note.channelId IS NULL') - .andWhere('note.userId = :meId', { meId: me.id }); + query.andWhere(new Brackets(qb => { + qb + .andWhere('note.channelId IS NULL') + .andWhere('note.userId = :meId', { meId: me.id }); + })); } query.andWhere(new Brackets(qb => { |