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>
This commit is contained in:
おさむのひと
2025-11-07 08:39:21 +09:00
committed by GitHub
parent e74ab35de3
commit 729abbef62
53 changed files with 3564 additions and 151 deletions

View File

@@ -18,6 +18,8 @@ import { QueryService } from '@/core/QueryService.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';
import { ApiError } from '../../error.js';
export const meta = {
@@ -46,7 +48,7 @@ export const meta = {
bothWithRepliesAndWithFiles: {
message: 'Specifying both withReplies and withFiles is not supported',
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f'
id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f',
},
},
} as const;
@@ -79,9 +81,6 @@ 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 roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
@@ -89,6 +88,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private cacheService: CacheService,
private queryService: QueryService,
private userFollowingService: UserFollowingService,
private channelMutingService: ChannelMutingService,
private channelFollowingService: ChannelFollowingService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -196,11 +197,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
withReplies: 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)));
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(new Brackets(qb => {
@@ -219,9 +222,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
if (followingChannels.length > 0) {
const followingChannelIds = followingChannels.map(x => x.followeeId);
if (followingChannelIds.length > 0) {
query.andWhere(new Brackets(qb => {
qb.where('note.channelId IN (:...followingChannelIds)', { followingChannelIds });
qb.orWhere('note.channelId IS NULL');
@@ -230,6 +231,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.andWhere('note.channelId IS NULL');
}
if (mutingChannelIds.length > 0) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteChannelId IS NULL');
qb.orWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
}));
}
if (!ps.withReplies) {
query.andWhere(new Brackets(qb => {
qb