diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2fa49e34565f..43c1f2fff60f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@
   - 詳細は #14730 および `.config/example.yml` または `.config/docker_example.yml`の'Fulltext search configuration'をご参照願います.
 
 ### General
+- Feat: チャンネルミュート機能の実装 #10649
+	- チャンネルの概要画面の右上からミュートできます(リンクコピー、共有、設定と同列)
 - Feat: カスタム絵文字管理画面をリニューアル #10996
 	* β版として公開のため、旧画面も引き続き利用可能です
 
diff --git a/packages/backend/jest.config.unit.cjs b/packages/backend/jest.config.unit.cjs
index aa5992936b42..957d0635c101 100644
--- a/packages/backend/jest.config.unit.cjs
+++ b/packages/backend/jest.config.unit.cjs
@@ -7,6 +7,7 @@ const base = require('./jest.config.cjs')
 
 module.exports = {
 	...base,
+	globalSetup: "<rootDir>/test/jest.setup.unit.cjs",
 	testMatch: [
 		"<rootDir>/test/unit/**/*.ts",
 		"<rootDir>/src/**/*.test.ts",
diff --git a/packages/backend/migration/1718015380000-add-channel-muting.js b/packages/backend/migration/1718015380000-add-channel-muting.js
new file mode 100644
index 000000000000..e2592dce7aeb
--- /dev/null
+++ b/packages/backend/migration/1718015380000-add-channel-muting.js
@@ -0,0 +1,42 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class AddChannelMuting1718015380000 {
+	name = 'AddChannelMuting1718015380000'
+
+	async up(queryRunner) {
+		await queryRunner.query(`
+			CREATE TABLE "channel_muting"
+			(
+				"id"        varchar(32) NOT NULL,
+				"userId"    varchar(32) NOT NULL,
+				"channelId" varchar(32) NOT NULL,
+				"expiresAt" timestamp with time zone,
+				CONSTRAINT "PK_channel_muting_id" PRIMARY KEY ("id"),
+				CONSTRAINT "FK_channel_muting_userId" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION,
+				CONSTRAINT "FK_channel_muting_channelId" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION
+			);
+			CREATE INDEX "IDX_channel_muting_userId" ON "channel_muting" ("userId");
+			CREATE INDEX "IDX_channel_muting_channelId" ON "channel_muting" ("channelId");
+
+			ALTER TABLE note ADD "renoteChannelId" varchar(32);
+			COMMENT ON COLUMN note."renoteChannelId" is '[Denormalized]';
+		`);
+	}
+
+	async down(queryRunner) {
+		await queryRunner.query(`
+			ALTER TABLE note DROP COLUMN "renoteChannelId";
+
+			ALTER TABLE "channel_muting"
+				DROP CONSTRAINT "FK_channel_muting_userId";
+			ALTER TABLE "channel_muting"
+				DROP CONSTRAINT "FK_channel_muting_channelId";
+			DROP INDEX "IDX_channel_muting_userId";
+			DROP INDEX "IDX_channel_muting_channelId";
+			DROP TABLE "channel_muting";
+		`);
+	}
+}
diff --git a/packages/backend/src/core/ChannelFollowingService.ts b/packages/backend/src/core/ChannelFollowingService.ts
index 12251595e2fa..d320a5ea3638 100644
--- a/packages/backend/src/core/ChannelFollowingService.ts
+++ b/packages/backend/src/core/ChannelFollowingService.ts
@@ -6,7 +6,7 @@
 import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
 import Redis from 'ioredis';
 import { DI } from '@/di-symbols.js';
-import type { ChannelFollowingsRepository } from '@/models/_.js';
+import type { ChannelFollowingsRepository, ChannelsRepository, MiUser } from '@/models/_.js';
 import { MiChannel } from '@/models/_.js';
 import { IdService } from '@/core/IdService.js';
 import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
@@ -23,6 +23,8 @@ export class ChannelFollowingService implements OnModuleInit {
 		private redisClient: Redis.Redis,
 		@Inject(DI.redisForSub)
 		private redisForSub: Redis.Redis,
+		@Inject(DI.channelsRepository)
+		private channelsRepository: ChannelsRepository,
 		@Inject(DI.channelFollowingsRepository)
 		private channelFollowingsRepository: ChannelFollowingsRepository,
 		private idService: IdService,
@@ -45,6 +47,50 @@ export class ChannelFollowingService implements OnModuleInit {
 	onModuleInit() {
 	}
 
+	/**
+	 * フォローしているチャンネルの一覧を取得する.
+	 * @param params
+	 * @param [opts]
+	 * @param	{(boolean|undefined)} [opts.idOnly=false] チャンネルIDのみを取得するかどうか. ID以外のフィールドに値がセットされなくなり、他テーブルとのJOINも一切されなくなるので注意.
+	 * @param {(boolean|undefined)} [opts.joinUser=undefined] チャンネルオーナーのユーザ情報をJOINするかどうか(falseまたは省略時はJOINしない).
+	 * @param {(boolean|undefined)} [opts.joinBannerFile=undefined] バナー画像のドライブファイルをJOINするかどうか(falseまたは省略時はJOINしない).
+	 */
+	@bindThis
+	public async list(
+		params: {
+			requestUserId: MiUser['id'],
+		},
+		opts?: {
+			idOnly?: boolean;
+			joinUser?: boolean;
+			joinBannerFile?: boolean;
+		},
+	): Promise<MiChannel[]> {
+		if (opts?.idOnly) {
+			const q = this.channelFollowingsRepository.createQueryBuilder('channel_following')
+				.select('channel_following.followeeId')
+				.where('channel_following.followerId = :userId', { userId: params.requestUserId });
+
+			return q
+				.getRawMany<{ channel_following_followeeId: string }>()
+				.then(xs => xs.map(x => ({ id: x.channel_following_followeeId } as MiChannel)));
+		} else {
+			const q = this.channelsRepository.createQueryBuilder('channel')
+				.innerJoin('channel_following', 'channel_following', 'channel_following.followeeId = channel.id')
+				.where('channel_following.followerId = :userId', { userId: params.requestUserId });
+
+			if (opts?.joinUser) {
+				q.innerJoinAndSelect('channel.user', 'user');
+			}
+
+			if (opts?.joinBannerFile) {
+				q.leftJoinAndSelect('channel.banner', 'drive_file');
+			}
+
+			return q.getMany();
+		}
+	}
+
 	@bindThis
 	public async follow(
 		requestUser: MiLocalUser,
diff --git a/packages/backend/src/core/ChannelMutingService.ts b/packages/backend/src/core/ChannelMutingService.ts
new file mode 100644
index 000000000000..bf5b848d4445
--- /dev/null
+++ b/packages/backend/src/core/ChannelMutingService.ts
@@ -0,0 +1,224 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import Redis from 'ioredis';
+import { Brackets, In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type { ChannelMutingRepository, ChannelsRepository, MiChannel, MiChannelMuting, MiUser } from '@/models/_.js';
+import { IdService } from '@/core/IdService.js';
+import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
+import { bindThis } from '@/decorators.js';
+import { RedisKVCache } from '@/misc/cache.js';
+
+@Injectable()
+export class ChannelMutingService {
+	public mutingChannelsCache: RedisKVCache<Set<string>>;
+
+	constructor(
+		@Inject(DI.redis)
+		private redisClient: Redis.Redis,
+		@Inject(DI.redisForSub)
+		private redisForSub: Redis.Redis,
+		@Inject(DI.channelsRepository)
+		private channelsRepository: ChannelsRepository,
+		@Inject(DI.channelMutingRepository)
+		private channelMutingRepository: ChannelMutingRepository,
+		private idService: IdService,
+		private globalEventService: GlobalEventService,
+	) {
+		this.mutingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'channelMutingChannels', {
+			lifetime: 1000 * 60 * 30, // 30m
+			memoryCacheLifetime: 1000 * 60, // 1m
+			fetcher: (userId) => this.channelMutingRepository.find({
+				where: { userId: userId },
+				select: ['channelId'],
+			}).then(xs => new Set(xs.map(x => x.channelId))),
+			toRedisConverter: (value) => JSON.stringify(Array.from(value)),
+			fromRedisConverter: (value) => new Set(JSON.parse(value)),
+		});
+
+		this.redisForSub.on('message', this.onMessage);
+	}
+
+	/**
+	 * ミュートしているチャンネルの一覧を取得する.
+	 * @param params
+	 * @param [opts]
+	 * @param	{(boolean|undefined)} [opts.idOnly=false] チャンネルIDのみを取得するかどうか. ID以外のフィールドに値がセットされなくなり、他テーブルとのJOINも一切されなくなるので注意.
+	 * @param {(boolean|undefined)} [opts.joinUser=undefined] チャンネルオーナーのユーザ情報をJOINするかどうか(falseまたは省略時はJOINしない).
+	 * @param {(boolean|undefined)} [opts.joinBannerFile=undefined] バナー画像のドライブファイルをJOINするかどうか(falseまたは省略時はJOINしない).
+	 */
+	@bindThis
+	public async list(
+		params: {
+			requestUserId: MiUser['id'],
+		},
+		opts?: {
+			idOnly?: boolean;
+			joinUser?: boolean;
+			joinBannerFile?: boolean;
+		},
+	): Promise<MiChannel[]> {
+		if (opts?.idOnly) {
+			const q = this.channelMutingRepository.createQueryBuilder('channel_muting')
+				.select('channel_muting.channelId')
+				.where('channel_muting.userId = :userId', { userId: params.requestUserId })
+				.andWhere(new Brackets(qb => {
+					qb.where('channel_muting.expiresAt IS NULL')
+						.orWhere('channel_muting.expiresAt > :now', { now: new Date() });
+				}));
+
+			return q
+				.getRawMany<{ channel_muting_channelId: string }>()
+				.then(xs => xs.map(x => ({ id: x.channel_muting_channelId } as MiChannel)));
+		} else {
+			const q = this.channelsRepository.createQueryBuilder('channel')
+				.innerJoin('channel_muting', 'channel_muting', 'channel_muting.channelId = channel.id')
+				.where('channel_muting.userId = :userId', { userId: params.requestUserId })
+				.andWhere(new Brackets(qb => {
+					qb.where('channel_muting.expiresAt IS NULL')
+						.orWhere('channel_muting.expiresAt > :now', { now: new Date() });
+				}));
+
+			if (opts?.joinUser) {
+				q.innerJoinAndSelect('channel.user', 'user');
+			}
+
+			if (opts?.joinBannerFile) {
+				q.leftJoinAndSelect('channel.banner', 'drive_file');
+			}
+
+			return q.getMany();
+		}
+	}
+
+	/**
+	 * 期限切れのチャンネルミュート情報を取得する.
+	 *
+	 * @param [opts]
+	 * @param {(boolean|undefined)} [opts.joinUser=undefined] チャンネルミュートを設定したユーザ情報をJOINするかどうか(falseまたは省略時はJOINしない).
+	 * @param {(boolean|undefined)} [opts.joinChannel=undefined] ミュート先のチャンネル情報をJOINするかどうか(falseまたは省略時はJOINしない).
+	 */
+	public async findExpiredMutings(opts?: {
+		joinUser?: boolean;
+		joinChannel?: boolean;
+	}): Promise<MiChannelMuting[]> {
+		const now = new Date();
+		const q = this.channelMutingRepository.createQueryBuilder('channel_muting')
+			.where('channel_muting.expiresAt < :now', { now });
+
+		if (opts?.joinUser) {
+			q.innerJoinAndSelect('channel_muting.user', 'user');
+		}
+
+		if (opts?.joinChannel) {
+			q.leftJoinAndSelect('channel_muting.channel', 'channel');
+		}
+
+		return q.getMany();
+	}
+
+	/**
+	 * 既にミュートされているかどうかをキャッシュから取得する.
+	 * @param params
+	 * @param params.requestUserId
+	 */
+	@bindThis
+	public async isMuted(params: {
+		requestUserId: MiUser['id'],
+		targetChannelId: MiChannel['id'],
+	}): Promise<boolean> {
+		const mutedChannels = await this.mutingChannelsCache.get(params.requestUserId);
+		return (mutedChannels?.has(params.targetChannelId) ?? false);
+	}
+
+	/**
+	 * チャンネルをミュートする.
+	 * @param params
+	 * @param {(Date|null|undefined)} [params.expiresAt] ミュートの有効期限. nullまたは省略時は無期限.
+	 */
+	@bindThis
+	public async mute(params: {
+		requestUserId: MiUser['id'],
+		targetChannelId: MiChannel['id'],
+		expiresAt?: Date | null,
+	}): Promise<void> {
+		await this.channelMutingRepository.insert({
+			id: this.idService.gen(),
+			userId: params.requestUserId,
+			channelId: params.targetChannelId,
+			expiresAt: params.expiresAt,
+		});
+
+		this.globalEventService.publishInternalEvent('muteChannel', {
+			userId: params.requestUserId,
+			channelId: params.targetChannelId,
+		});
+	}
+
+	/**
+	 * チャンネルのミュートを解除する.
+	 * @param params
+	 */
+	@bindThis
+	public async unmute(params: {
+		requestUserId: MiUser['id'],
+		targetChannelId: MiChannel['id'],
+	}): Promise<void> {
+		await this.channelMutingRepository.delete({
+			userId: params.requestUserId,
+			channelId: params.targetChannelId,
+		});
+
+		this.globalEventService.publishInternalEvent('unmuteChannel', {
+			userId: params.requestUserId,
+			channelId: params.targetChannelId,
+		});
+	}
+
+	/**
+	 * 期限切れのチャンネルミュート情報を削除する.
+	 */
+	@bindThis
+	public async eraseExpiredMutings(): Promise<void> {
+		const expiredMutings = await this.findExpiredMutings();
+		await this.channelMutingRepository.delete({ id: In(expiredMutings.map(x => x.id)) });
+
+		const userIds = [...new Set(expiredMutings.map(x => x.userId))];
+		for (const userId of userIds) {
+			this.mutingChannelsCache.refresh(userId).then();
+		}
+	}
+
+	@bindThis
+	private async onMessage(_: string, data: string): Promise<void> {
+		const obj = JSON.parse(data);
+
+		if (obj.channel === 'internal') {
+			const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+			switch (type) {
+				case 'muteChannel': {
+					this.mutingChannelsCache.refresh(body.userId).then();
+					break;
+				}
+				case 'unmuteChannel': {
+					this.mutingChannelsCache.delete(body.userId).then();
+					break;
+				}
+			}
+		}
+	}
+
+	@bindThis
+	public dispose(): void {
+		this.mutingChannelsCache.dispose();
+	}
+
+	@bindThis
+	public onApplicationShutdown(signal?: string | undefined): void {
+		this.dispose();
+	}
+}
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 734d135648d5..db4441c5a3bb 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -15,6 +15,7 @@ import { SystemWebhookService } from '@/core/SystemWebhookService.js';
 import { UserSearchService } from '@/core/UserSearchService.js';
 import { WebhookTestService } from '@/core/WebhookTestService.js';
 import { FlashService } from '@/core/FlashService.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
 import { AccountMoveService } from './AccountMoveService.js';
 import { AccountUpdateService } from './AccountUpdateService.js';
 import { AiService } from './AiService.js';
@@ -225,6 +226,7 @@ const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: Fe
 const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService };
 const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService };
 const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
+const $ChannelMutingService: Provider = { provide: 'ChannelMutingService', useExisting: ChannelMutingService };
 const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
 const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
 
@@ -376,6 +378,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		FanoutTimelineService,
 		FanoutTimelineEndpointService,
 		ChannelFollowingService,
+		ChannelMutingService,
 		RegistryApiService,
 		ReversiService,
 
@@ -523,6 +526,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$FanoutTimelineService,
 		$FanoutTimelineEndpointService,
 		$ChannelFollowingService,
+		$ChannelMutingService,
 		$RegistryApiService,
 		$ReversiService,
 
@@ -671,6 +675,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		FanoutTimelineService,
 		FanoutTimelineEndpointService,
 		ChannelFollowingService,
+		ChannelMutingService,
 		RegistryApiService,
 		ReversiService,
 
@@ -816,6 +821,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 		$FanoutTimelineService,
 		$FanoutTimelineEndpointService,
 		$ChannelFollowingService,
+		$ChannelMutingService,
 		$RegistryApiService,
 		$ReversiService,
 
diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index b05af99c5e5e..87bc5e79f0fd 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -17,6 +17,8 @@ import { isQuote, isRenote } from '@/misc/is-renote.js';
 import { CacheService } from '@/core/CacheService.js';
 import { isReply } from '@/misc/is-reply.js';
 import { isInstanceMuted } from '@/misc/is-instance-muted.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
+import { isChannelRelated } from '@/misc/is-channel-related.js';
 
 type TimelineOptions = {
 	untilId: string | null,
@@ -33,6 +35,7 @@ type TimelineOptions = {
 	excludeNoFiles?: boolean;
 	excludeReplies?: boolean;
 	excludePureRenotes: boolean;
+	includeMutedChannels?: boolean;
 	dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise<MiNote[]>,
 };
 
@@ -45,6 +48,7 @@ export class FanoutTimelineEndpointService {
 		private noteEntityService: NoteEntityService,
 		private cacheService: CacheService,
 		private fanoutTimelineService: FanoutTimelineService,
+		private channelMutingService: ChannelMutingService,
 	) {
 	}
 
@@ -101,11 +105,13 @@ export class FanoutTimelineEndpointService {
 					userIdsWhoMeMutingRenotes,
 					userIdsWhoBlockingMe,
 					userMutedInstances,
+					userMutedChannels,
 				] = 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)),
+					ps.includeMutedChannels ? Promise.resolve(new Set<string>()) : this.channelMutingService.mutingChannelsCache.fetch(me.id),
 				]);
 
 				const parentFilter = filter;
@@ -114,6 +120,7 @@ export class FanoutTimelineEndpointService {
 					if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
 					if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false;
 					if (isInstanceMuted(note, userMutedInstances)) return false;
+					if (!ps.includeMutedChannels && isChannelRelated(note, userMutedChannels)) return false;
 
 					return parentFilter(note);
 				};
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 03646ff56680..2b48416a5b3d 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -244,6 +244,8 @@ export interface InternalEventTypes {
 	metaUpdated: { before?: MiMeta; after: MiMeta; };
 	followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
 	unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
+	muteChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
+	unmuteChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
 	updateUserProfile: MiUserProfile;
 	mute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
 	unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 8a79908e8263..3a83f9018947 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -438,6 +438,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 			replyUserHost: data.reply ? data.reply.userHost : null,
 			renoteUserId: data.renote ? data.renote.userId : null,
 			renoteUserHost: data.renote ? data.renote.userHost : null,
+			renoteChannelId: data.renote ? data.renote.channelId : null,
 			userHost: user.host,
 		});
 
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
index 555a39f71c16..ed489076e331 100644
--- a/packages/backend/src/core/WebhookTestService.ts
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -131,6 +131,7 @@ function generateDummyNote(override?: Partial<MiNote>): MiNote {
 		replyUserHost: null,
 		renoteUserId: null,
 		renoteUserHost: null,
+		renoteChannelId: null,
 		...override,
 	};
 }
diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts
index 1ba7ca8e57e7..9ee918bea370 100644
--- a/packages/backend/src/core/entities/ChannelEntityService.ts
+++ b/packages/backend/src/core/entities/ChannelEntityService.ts
@@ -4,36 +4,40 @@
  */
 
 import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
 import { DI } from '@/di-symbols.js';
-import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NotesRepository } from '@/models/_.js';
+import type {
+	ChannelFavoritesRepository,
+	ChannelFollowingsRepository, ChannelMutingRepository,
+	ChannelsRepository,
+	DriveFilesRepository,
+	MiDriveFile,
+	MiNote,
+	NotesRepository,
+} from '@/models/_.js';
 import type { Packed } from '@/misc/json-schema.js';
-import type { } from '@/models/Blocking.js';
 import type { MiUser } from '@/models/User.js';
 import type { MiChannel } from '@/models/Channel.js';
 import { bindThis } from '@/decorators.js';
 import { IdService } from '@/core/IdService.js';
 import { DriveFileEntityService } from './DriveFileEntityService.js';
 import { NoteEntityService } from './NoteEntityService.js';
-import { In } from 'typeorm';
 
 @Injectable()
 export class ChannelEntityService {
 	constructor(
 		@Inject(DI.channelsRepository)
 		private channelsRepository: ChannelsRepository,
-
 		@Inject(DI.channelFollowingsRepository)
 		private channelFollowingsRepository: ChannelFollowingsRepository,
-
 		@Inject(DI.channelFavoritesRepository)
 		private channelFavoritesRepository: ChannelFavoritesRepository,
-
+		@Inject(DI.channelMutingRepository)
+		private channelMutingRepository: ChannelMutingRepository,
 		@Inject(DI.notesRepository)
 		private notesRepository: NotesRepository,
-
 		@Inject(DI.driveFilesRepository)
 		private driveFilesRepository: DriveFilesRepository,
-
 		private noteEntityService: NoteEntityService,
 		private driveFileEntityService: DriveFileEntityService,
 		private idService: IdService,
@@ -45,31 +49,59 @@ export class ChannelEntityService {
 		src: MiChannel['id'] | MiChannel,
 		me?: { id: MiUser['id'] } | null | undefined,
 		detailed?: boolean,
+		opts?: {
+			bannerFiles?: Map<MiDriveFile['id'], MiDriveFile>;
+			followings?: Set<MiChannel['id']>;
+			favorites?: Set<MiChannel['id']>;
+			muting?: Set<MiChannel['id']>;
+			pinnedNotes?: Map<MiNote['id'], MiNote>;
+		},
 	): Promise<Packed<'Channel'>> {
 		const channel = typeof src === 'object' ? src : await this.channelsRepository.findOneByOrFail({ id: src });
-		const meId = me ? me.id : null;
-
-		const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
-
-		const isFollowing = meId ? await this.channelFollowingsRepository.exists({
-			where: {
-				followerId: meId,
-				followeeId: channel.id,
-			},
-		}) : false;
-
-		const isFavorited = meId ? await this.channelFavoritesRepository.exists({
-			where: {
-				userId: meId,
-				channelId: channel.id,
-			},
-		}) : false;
-
-		const pinnedNotes = channel.pinnedNoteIds.length > 0 ? await this.notesRepository.find({
-			where: {
-				id: In(channel.pinnedNoteIds),
-			},
-		}) : [];
+
+		let bannerFile: MiDriveFile | null = null;
+		if (channel.bannerId) {
+			bannerFile = opts?.bannerFiles?.get(channel.bannerId)
+				?? await this.driveFilesRepository.findOneByOrFail({ id: channel.bannerId });
+		}
+
+		let isFollowing = false;
+		let isFavorited = false;
+		let isMuting = false;
+		if (me) {
+			isFollowing = opts?.followings?.has(channel.id) ?? await this.channelFollowingsRepository.exists({
+				where: {
+					followerId: me.id,
+					followeeId: channel.id,
+				},
+			});
+
+			isFavorited = opts?.favorites?.has(channel.id) ?? await this.channelFavoritesRepository.exists({
+				where: {
+					userId: me.id,
+					channelId: channel.id,
+				},
+			});
+
+			isMuting = opts?.muting?.has(channel.id) ?? await this.channelMutingRepository.exists({
+				where: {
+					userId: me.id,
+					channelId: channel.id,
+				},
+			});
+		}
+
+		const pinnedNotes = Array.of<MiNote>();
+		if (channel.pinnedNoteIds.length > 0) {
+			pinnedNotes.push(
+				...(
+					opts?.pinnedNotes
+						// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+						? channel.pinnedNoteIds.map(it => opts.pinnedNotes!.get(it)).filter(it => it != null)
+						: await this.notesRepository.findBy({ id: In(channel.pinnedNoteIds) })
+				),
+			);
+		}
 
 		return {
 			id: channel.id,
@@ -78,7 +110,7 @@ export class ChannelEntityService {
 			name: channel.name,
 			description: channel.description,
 			userId: channel.userId,
-			bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
+			bannerUrl: bannerFile ? this.driveFileEntityService.getPublicUrl(bannerFile) : null,
 			pinnedNoteIds: channel.pinnedNoteIds,
 			color: channel.color,
 			isArchived: channel.isArchived,
@@ -90,6 +122,7 @@ export class ChannelEntityService {
 			...(me ? {
 				isFollowing,
 				isFavorited,
+				isMuting,
 				hasUnreadNote: false, // 後方互換性のため
 			} : {}),
 
@@ -98,5 +131,72 @@ export class ChannelEntityService {
 			} : {}),
 		};
 	}
+
+	@bindThis
+	public async packMany(
+		src: MiChannel['id'][] | MiChannel[],
+		me?: { id: MiUser['id'] } | null | undefined,
+		detailed?: boolean,
+	): Promise<Packed<'Channel'>[]> {
+		// IDのみの要素がある場合、DBからオブジェクトを取得して補う
+		const channels = src.filter(it => typeof it === 'object') as MiChannel[];
+		channels.push(
+			...(await this.channelsRepository.find({
+				where: {
+					id: In(src.filter(it => typeof it !== 'object') as MiChannel['id'][]),
+				},
+			})),
+		);
+		channels.sort((a, b) => a.id.localeCompare(b.id));
+
+		const bannerFiles = await this.driveFilesRepository
+			.findBy({
+				id: In(channels.map(it => it.bannerId).filter(it => it != null)),
+			})
+			.then(it => new Map(it.map(it => [it.id, it])));
+
+		const followings = me
+			? await this.channelFollowingsRepository
+				.findBy({
+					followerId: me.id,
+					followeeId: In(channels.map(it => it.id)),
+				})
+				.then(it => new Set(it.map(it => it.followeeId)))
+			: new Set<MiChannel['id']>();
+
+		const favorites = me
+			? await this.channelFavoritesRepository
+				.findBy({
+					userId: me.id,
+					channelId: In(channels.map(it => it.id)),
+				})
+				.then(it => new Set(it.map(it => it.channelId)))
+			: new Set<MiChannel['id']>();
+
+		const muting = me
+			? await this.channelMutingRepository
+				.findBy({
+					userId: me.id,
+					channelId: In(channels.map(it => it.id)),
+				})
+				.then(it => new Set(it.map(it => it.channelId)))
+			: new Set<MiChannel['id']>();
+
+		const pinnedNotes = await this.notesRepository
+			.find({
+				where: {
+					id: In(channels.flatMap(it => it.pinnedNoteIds)),
+				},
+			})
+			.then(it => new Map(it.map(it => [it.id, it])));
+
+		return Promise.all(channels.map(it => this.pack(it, me, detailed, {
+			bannerFiles,
+			followings,
+			favorites,
+			muting,
+			pinnedNotes,
+		})));
+	}
 }
 
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index e599fc7b3737..6d307ec74ec5 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -71,6 +71,7 @@ export const DI = {
 	channelsRepository: Symbol('channelsRepository'),
 	channelFollowingsRepository: Symbol('channelFollowingsRepository'),
 	channelFavoritesRepository: Symbol('channelFavoritesRepository'),
+	channelMutingRepository: Symbol('channelMutingRepository'),
 	registryItemsRepository: Symbol('registryItemsRepository'),
 	webhooksRepository: Symbol('webhooksRepository'),
 	systemWebhooksRepository: Symbol('systemWebhooksRepository'),
diff --git a/packages/backend/src/misc/is-channel-related.ts b/packages/backend/src/misc/is-channel-related.ts
new file mode 100644
index 000000000000..4b6614cd286f
--- /dev/null
+++ b/packages/backend/src/misc/is-channel-related.ts
@@ -0,0 +1,28 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { MiNote } from '@/models/Note.js';
+import { Packed } from '@/misc/json-schema.js';
+
+/**
+ * {@link note}が{@link channelIds}のチャンネルに関連するかどうかを判定し、関連する場合はtrueを返します。
+ * 関連するというのは、{@link channelIds}のチャンネルに向けての投稿であるか、またはそのチャンネルの投稿をリノート・引用リノートした投稿であるかを指します。
+ *
+ * @param note 確認対象のノート
+ * @param channelIds 確認対象のチャンネルID一覧
+ */
+export function isChannelRelated(note: MiNote | Packed<'Note'>, channelIds: Set<string>): boolean {
+	if (note.channelId && channelIds.has(note.channelId)) {
+		return true;
+	}
+
+	if (note.renote != null && note.renote.channelId && channelIds.has(note.renote.channelId)) {
+		return true;
+	}
+
+	// NOTE: リプライはchannelIdのチェックだけでOKなはずなので見てない(チャンネルのノートにチャンネル外からのリプライまたはその逆はないはずなので)
+
+	return false;
+}
diff --git a/packages/backend/src/models/ChannelMuting.ts b/packages/backend/src/models/ChannelMuting.ts
new file mode 100644
index 000000000000..11ac7e5cefd7
--- /dev/null
+++ b/packages/backend/src/models/ChannelMuting.ts
@@ -0,0 +1,46 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
+import { id } from './util/id.js';
+import { MiUser } from './User.js';
+import { MiChannel } from './Channel.js';
+
+@Entity('channel_muting')
+@Index(['userId', 'channelId'], {})
+export class MiChannelMuting {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column({
+		...id(),
+	})
+	public userId: MiUser['id'];
+
+	@ManyToOne(type => MiUser, {
+		onDelete: 'CASCADE',
+	})
+	@JoinColumn()
+	public user: MiUser | null;
+
+	@Index()
+	@Column({
+		...id(),
+	})
+	public channelId: MiChannel['id'];
+
+	@ManyToOne(type => MiChannel, {
+		onDelete: 'CASCADE',
+	})
+	@JoinColumn()
+	public channel: MiChannel | null;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		nullable: true,
+	})
+	public expiresAt: Date | null;
+}
diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts
index 9a95c6faab9b..7741ce47a3e8 100644
--- a/packages/backend/src/models/Note.ts
+++ b/packages/backend/src/models/Note.ts
@@ -229,6 +229,13 @@ export class MiNote {
 		comment: '[Denormalized]',
 	})
 	public renoteUserHost: string | null;
+
+	@Column({
+		...id(),
+		nullable: true,
+		comment: '[Denormalized]',
+	})
+	public renoteChannelId: MiChannel['id'] | null;
 	//#endregion
 
 	constructor(data: Partial<MiNote>) {
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index ea0f88babaa7..dfcbb7ceb4b0 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -22,6 +22,7 @@ import {
 	MiChannel,
 	MiChannelFavorite,
 	MiChannelFollowing,
+	MiChannelMuting,
 	MiClip,
 	MiClipFavorite,
 	MiClipNote,
@@ -417,6 +418,12 @@ const $channelFavoritesRepository: Provider = {
 	inject: [DI.db],
 };
 
+const $channelMutingRepository: Provider = {
+	provide: DI.channelMutingRepository,
+	useFactory: (db: DataSource) => db.getRepository(MiChannelMuting).extend(miRepository as MiRepository<MiChannelMuting>),
+	inject: [DI.db],
+};
+
 const $registryItemsRepository: Provider = {
 	provide: DI.registryItemsRepository,
 	useFactory: (db: DataSource) => db.getRepository(MiRegistryItem).extend(miRepository as MiRepository<MiRegistryItem>),
@@ -554,6 +561,7 @@ const $reversiGamesRepository: Provider = {
 		$channelsRepository,
 		$channelFollowingsRepository,
 		$channelFavoritesRepository,
+		$channelMutingRepository,
 		$registryItemsRepository,
 		$webhooksRepository,
 		$systemWebhooksRepository,
@@ -625,6 +633,7 @@ const $reversiGamesRepository: Provider = {
 		$channelsRepository,
 		$channelFollowingsRepository,
 		$channelFavoritesRepository,
+		$channelMutingRepository,
 		$registryItemsRepository,
 		$webhooksRepository,
 		$systemWebhooksRepository,
diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts
index c72bdaa72726..abfc5b11f2c5 100644
--- a/packages/backend/src/models/_.ts
+++ b/packages/backend/src/models/_.ts
@@ -3,13 +3,10 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder, TypeORMError } from 'typeorm';
-import { DriverUtils } from 'typeorm/driver/DriverUtils.js';
+import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
 import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js';
 import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js';
 import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
-import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
-import { OrmUtils } from 'typeorm/util/OrmUtils.js';
 import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
 import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
 import { MiAccessToken } from '@/models/AccessToken.js';
@@ -23,6 +20,7 @@ import { MiAuthSession } from '@/models/AuthSession.js';
 import { MiBlocking } from '@/models/Blocking.js';
 import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
 import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
+import { MiChannelMuting } from "@/models/ChannelMuting.js";
 import { MiClip } from '@/models/Clip.js';
 import { MiClipNote } from '@/models/ClipNote.js';
 import { MiClipFavorite } from '@/models/ClipFavorite.js';
@@ -138,6 +136,7 @@ export {
 	MiBlocking,
 	MiChannelFollowing,
 	MiChannelFavorite,
+	MiChannelMuting,
 	MiClip,
 	MiClipNote,
 	MiClipFavorite,
@@ -209,6 +208,7 @@ export type AuthSessionsRepository = Repository<MiAuthSession> & MiRepository<Mi
 export type BlockingsRepository = Repository<MiBlocking> & MiRepository<MiBlocking>;
 export type ChannelFollowingsRepository = Repository<MiChannelFollowing> & MiRepository<MiChannelFollowing>;
 export type ChannelFavoritesRepository = Repository<MiChannelFavorite> & MiRepository<MiChannelFavorite>;
+export type ChannelMutingRepository = Repository<MiChannelMuting> & MiRepository<MiChannelMuting>;
 export type ClipsRepository = Repository<MiClip> & MiRepository<MiClip>;
 export type ClipNotesRepository = Repository<MiClipNote> & MiRepository<MiClipNote>;
 export type ClipFavoritesRepository = Repository<MiClipFavorite> & MiRepository<MiClipFavorite>;
diff --git a/packages/backend/src/models/json-schema/channel.ts b/packages/backend/src/models/json-schema/channel.ts
index d233f7858d9a..a7966ffdb362 100644
--- a/packages/backend/src/models/json-schema/channel.ts
+++ b/packages/backend/src/models/json-schema/channel.ts
@@ -80,6 +80,10 @@ export const packedChannelSchema = {
 			type: 'boolean',
 			optional: true, nullable: false,
 		},
+		isMuting: {
+			type: 'boolean',
+			optional: true, nullable: false,
+		},
 		pinnedNotes: {
 			type: 'array',
 			optional: true, nullable: false,
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index d09240eba1c9..c42c225efae6 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -22,6 +22,7 @@ import { MiAuthSession } from '@/models/AuthSession.js';
 import { MiBlocking } from '@/models/Blocking.js';
 import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
 import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
+import { MiChannelMuting } from "@/models/ChannelMuting.js";
 import { MiClip } from '@/models/Clip.js';
 import { MiClipNote } from '@/models/ClipNote.js';
 import { MiClipFavorite } from '@/models/ClipFavorite.js';
@@ -221,6 +222,7 @@ export const entities = [
 	MiChannel,
 	MiChannelFollowing,
 	MiChannelFavorite,
+	MiChannelMuting,
 	MiRegistryItem,
 	MiAd,
 	MiPasswordResetRequest,
diff --git a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts
index 448fc9c7633d..e898e6dd489e 100644
--- a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts
+++ b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts
@@ -4,14 +4,13 @@
  */
 
 import { Inject, Injectable } from '@nestjs/common';
-import { In } from 'typeorm';
 import { DI } from '@/di-symbols.js';
 import type { MutingsRepository } from '@/models/_.js';
 import type Logger from '@/logger.js';
 import { bindThis } from '@/decorators.js';
 import { UserMutingService } from '@/core/UserMutingService.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
-import type * as Bull from 'bullmq';
 
 @Injectable()
 export class CheckExpiredMutingsProcessorService {
@@ -22,6 +21,7 @@ export class CheckExpiredMutingsProcessorService {
 		private mutingsRepository: MutingsRepository,
 
 		private userMutingService: UserMutingService,
+		private channelMutingService: ChannelMutingService,
 		private queueLoggerService: QueueLoggerService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('check-expired-mutings');
@@ -41,6 +41,8 @@ export class CheckExpiredMutingsProcessorService {
 			await this.userMutingService.unmute(expired);
 		}
 
+		await this.channelMutingService.eraseExpiredMutings();
+
 		this.logger.succ('All expired mutings checked.');
 	}
 }
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 87c9841fd0c3..73c04d59ffb0 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -130,6 +130,9 @@ import * as ep___channels_favorite from './endpoints/channels/favorite.js';
 import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
 import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
 import * as ep___channels_search from './endpoints/channels/search.js';
+import * as ep___channels_mute_create from './endpoints/channels/mute/create.js';
+import * as ep___channels_mute_delete from './endpoints/channels/mute/delete.js';
+import * as ep___channels_mute_list from './endpoints/channels/mute/list.js';
 import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
 import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
 import * as ep___charts_drive from './endpoints/charts/drive.js';
@@ -521,6 +524,9 @@ const $channels_favorite: Provider = { provide: 'ep:channels/favorite', useClass
 const $channels_unfavorite: Provider = { provide: 'ep:channels/unfavorite', useClass: ep___channels_unfavorite.default };
 const $channels_myFavorites: Provider = { provide: 'ep:channels/my-favorites', useClass: ep___channels_myFavorites.default };
 const $channels_search: Provider = { provide: 'ep:channels/search', useClass: ep___channels_search.default };
+const $channels_mute_create: Provider = { provide: 'ep:channels/mute/create', useClass: ep___channels_mute_create.default };
+const $channels_mute_delete: Provider = { provide: 'ep:channels/mute/delete', useClass: ep___channels_mute_delete.default };
+const $channels_mute_list: Provider = { provide: 'ep:channels/mute/list', useClass: ep___channels_mute_list.default };
 const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useClass: ep___charts_activeUsers.default };
 const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default };
 const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default };
@@ -916,6 +922,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 		$channels_unfavorite,
 		$channels_myFavorites,
 		$channels_search,
+		$channels_mute_create,
+		$channels_mute_delete,
+		$channels_mute_list,
 		$charts_activeUsers,
 		$charts_apRequest,
 		$charts_drive,
@@ -1305,6 +1314,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
 		$channels_unfavorite,
 		$channels_myFavorites,
 		$channels_search,
+		$channels_mute_create,
+		$channels_mute_delete,
+		$channels_mute_list,
 		$charts_activeUsers,
 		$charts_apRequest,
 		$charts_drive,
diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts
index b8f448477b06..429bc9a17967 100644
--- a/packages/backend/src/server/api/StreamingApiServerService.ts
+++ b/packages/backend/src/server/api/StreamingApiServerService.ts
@@ -16,6 +16,7 @@ import { CacheService } from '@/core/CacheService.js';
 import { MiLocalUser } from '@/models/User.js';
 import { UserService } from '@/core/UserService.js';
 import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
 import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
 import MainStreamConnection from './stream/Connection.js';
 import { ChannelsService } from './stream/ChannelsService.js';
@@ -41,6 +42,7 @@ export class StreamingApiServerService {
 		private notificationService: NotificationService,
 		private usersService: UserService,
 		private channelFollowingService: ChannelFollowingService,
+		private channelMutingService: ChannelMutingService,
 	) {
 	}
 
@@ -100,6 +102,7 @@ export class StreamingApiServerService {
 				this.notificationService,
 				this.cacheService,
 				this.channelFollowingService,
+				this.channelMutingService,
 				user, app,
 			);
 
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 4d0c45cc91d8..0c740651fc10 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -136,6 +136,9 @@ import * as ep___channels_favorite from './endpoints/channels/favorite.js';
 import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
 import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
 import * as ep___channels_search from './endpoints/channels/search.js';
+import * as ep___channels_mute_create from './endpoints/channels/mute/create.js';
+import * as ep___channels_mute_delete from './endpoints/channels/mute/delete.js';
+import * as ep___channels_mute_list from './endpoints/channels/mute/list.js';
 import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
 import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
 import * as ep___charts_drive from './endpoints/charts/drive.js';
@@ -525,6 +528,9 @@ const eps = [
 	['channels/unfavorite', ep___channels_unfavorite],
 	['channels/my-favorites', ep___channels_myFavorites],
 	['channels/search', ep___channels_search],
+	['channels/mute/create', ep___channels_mute_create],
+	['channels/mute/delete', ep___channels_mute_delete],
+	['channels/mute/list', ep___channels_mute_list],
 	['charts/active-users', ep___charts_activeUsers],
 	['charts/ap-request', ep___charts_apRequest],
 	['charts/drive', ep___charts_drive],
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
index f4dfe1ecc4d7..49f1df969227 100644
--- a/packages/backend/src/server/api/endpoints/antennas/notes.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -5,6 +5,7 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import * as Redis from 'ioredis';
+import { Brackets } from 'typeorm';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { NotesRepository, AntennasRepository } from '@/models/_.js';
 import { QueryService } from '@/core/QueryService.js';
@@ -15,6 +16,7 @@ import { IdService } from '@/core/IdService.js';
 import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
 import { trackPromise } from '@/misc/promise-tracker.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -74,6 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private noteReadService: NoteReadService,
 		private fanoutTimelineService: FanoutTimelineService,
 		private globalEventService: GlobalEventService,
+		private channelMutingService: ChannelMutingService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -113,6 +116,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				.leftJoinAndSelect('reply.user', 'replyUser')
 				.leftJoinAndSelect('renote.user', 'renoteUser');
 
+			// -- ミュートされたチャンネル対策
+			const mutingChannelIds = await this.channelMutingService
+				.list({ requestUserId: me.id }, { idOnly: true })
+				.then(x => x.map(x => x.id));
+			if (mutingChannelIds.length > 0) {
+				query.andWhere(new Brackets(qb => {
+					qb.orWhere('note.channelId IS NULL');
+					qb.orWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
+				}));
+				query.andWhere(new Brackets(qb => {
+					qb.orWhere('note.renoteChannelId IS NULL');
+					qb.orWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
+				}));
+			}
+
 			this.queryService.generateVisibilityQuery(query, me);
 			this.queryService.generateMutedUserQuery(query, me);
 			this.queryService.generateBlockedUserQuery(query, me);
diff --git a/packages/backend/src/server/api/endpoints/channels/mute/create.ts b/packages/backend/src/server/api/endpoints/channels/mute/create.ts
new file mode 100644
index 000000000000..26ce707c7a41
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/mute/create.ts
@@ -0,0 +1,90 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { ChannelsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
+
+export const meta = {
+	tags: ['channels', 'mute'],
+
+	requireCredential: true,
+	prohibitMoved: true,
+
+	kind: 'write:channels',
+
+	errors: {
+		noSuchChannel: {
+			message: 'No such Channel.',
+			code: 'NO_SUCH_CHANNEL',
+			id: '7174361e-d58f-31d6-2e7c-6fb830786a3f',
+		},
+
+		alreadyMuting: {
+			message: 'You are already muting that user.',
+			code: 'ALREADY_MUTING_CHANNEL',
+			id: '5a251978-769a-da44-3e89-3931e43bb592',
+		},
+
+		expiresAtIsPast: {
+			message: 'Cannot set past date to "expiresAt".',
+			code: 'EXPIRES_AT_IS_PAST',
+			id: '42b32236-df2c-a45f-fdbf-def67268f749',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		channelId: { type: 'string', format: 'misskey:id' },
+		expiresAt: {
+			type: 'integer',
+			nullable: true,
+			description: 'A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute.',
+		},
+	},
+	required: ['channelId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		@Inject(DI.channelsRepository)
+		private channelsRepository: ChannelsRepository,
+		private channelMutingService: ChannelMutingService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			// Check if exists the channel
+			const targetChannel = await this.channelsRepository.findOneBy({ id: ps.channelId });
+			if (!targetChannel) {
+				throw new ApiError(meta.errors.noSuchChannel);
+			}
+
+			// Check if already muting
+			const exist = await this.channelMutingService.isMuted({
+				requestUserId: me.id,
+				targetChannelId: targetChannel.id,
+			});
+			if (exist) {
+				throw new ApiError(meta.errors.alreadyMuting);
+			}
+
+			// Check if expiresAt is past
+			if (ps.expiresAt && ps.expiresAt <= Date.now()) {
+				throw new ApiError(meta.errors.expiresAtIsPast);
+			}
+
+			await this.channelMutingService.mute({
+				requestUserId: me.id,
+				targetChannelId: targetChannel.id,
+				expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
+			});
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/channels/mute/delete.ts b/packages/backend/src/server/api/endpoints/channels/mute/delete.ts
new file mode 100644
index 000000000000..79abeebe997c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/mute/delete.ts
@@ -0,0 +1,73 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { ChannelsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+	tags: ['channels', 'mute'],
+
+	requireCredential: true,
+	prohibitMoved: true,
+
+	kind: 'write:channels',
+
+	errors: {
+		noSuchChannel: {
+			message: 'No such Channel.',
+			code: 'NO_SUCH_CHANNEL',
+			id: 'e7998769-6e94-d9c2-6b8f-94a527314aba',
+		},
+
+		notMuting: {
+			message: 'You are not muting that channel.',
+			code: 'NOT_MUTING_CHANNEL',
+			id: '14d55962-6ea8-d990-1333-d6bef78dc2ab',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		channelId: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['channelId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		@Inject(DI.channelsRepository)
+		private channelsRepository: ChannelsRepository,
+		private channelMutingService: ChannelMutingService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			// Check if exists the channel
+			const targetChannel = await this.channelsRepository.findOneBy({ id: ps.channelId });
+			if (!targetChannel) {
+				throw new ApiError(meta.errors.noSuchChannel);
+			}
+
+			// Check muting
+			const exist = await this.channelMutingService.isMuted({
+				requestUserId: me.id,
+				targetChannelId: targetChannel.id,
+			});
+			if (!exist) {
+				throw new ApiError(meta.errors.notMuting);
+			}
+
+			await this.channelMutingService.unmute({
+				requestUserId: me.id,
+				targetChannelId: targetChannel.id,
+			});
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/channels/mute/list.ts b/packages/backend/src/server/api/endpoints/channels/mute/list.ts
new file mode 100644
index 000000000000..74338eea388f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/mute/list.ts
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
+import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
+
+export const meta = {
+	tags: ['channels', 'mute'],
+
+	requireCredential: true,
+	prohibitMoved: true,
+
+	kind: 'read:channels',
+
+	res: {
+		type: 'array',
+		optional: false, nullable: false,
+		items: {
+			type: 'object',
+			optional: false, nullable: false,
+			ref: 'Channel',
+		},
+	},
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {},
+	required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+	constructor(
+		private channelMutingService: ChannelMutingService,
+		private channelEntityService: ChannelEntityService,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const mutings = await this.channelMutingService.list({
+				requestUserId: me.id,
+			});
+			return await this.channelEntityService.packMany(mutings, me);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index d4fd75e04955..b81828234117 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -13,6 +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 { ChannelMutingService } from '@/core/ChannelMutingService.js';
+import { isChannelRelated } from '@/misc/is-channel-related.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -70,6 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private queryService: QueryService,
 		private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
 		private activeUsersChart: ActiveUsersChart,
+		private channelMutingService: ChannelMutingService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -89,6 +92,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me);
 			}
 
+			const mutingChannelIds = me
+				? await this.channelMutingService.mutingChannelsCache.get(me.id) ?? new Set<string>()
+				: new Set<string>();
 			return await this.fanoutTimelineEndpointService.timeline({
 				untilId,
 				sinceId,
@@ -98,6 +104,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				useDbFallback: true,
 				redisTimelines: [`channelTimeline:${channel.id}`],
 				excludePureRenotes: false,
+				includeMutedChannels: true,
+				noteFilter: note => {
+					// 共通機能を使うと見ているチャンネルそのものもミュートしてしまうので閲覧中のチャンネル以外を除く形にする
+					if (note.channelId === channel.id && (note.renoteChannelId === null || note.renoteChannelId === channel.id)) return true;
+					return !isChannelRelated(note, mutingChannelIds);
+				},
 				dbFallback: async (untilId, sinceId, limit) => {
 					return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me);
 				},
@@ -122,6 +134,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			.leftJoinAndSelect('note.channel', 'channel');
 
 		if (me) {
+			const mutingChannelIds = await this.channelMutingService
+				.list({ requestUserId: me.id }, { idOnly: true })
+				.then(x => x.map(x => x.id).filter(x => x !== ps.channelId));
+			if (mutingChannelIds.length > 0) {
+				query.andWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
+				query.andWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
+			}
+
 			this.queryService.generateMutedUserQuery(query, me);
 			this.queryService.generateBlockedUserQuery(query, 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 aed9065bf953..d514f5bb438b 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -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
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 0b48f2c78bd4..9c4812cbbfee 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 { ChannelMutingService } from '@/core/ChannelMutingService.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -76,6 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private idService: IdService,
 		private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
 		private queryService: QueryService,
+		private channelMutingService: ChannelMutingService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -156,9 +158,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			.leftJoinAndSelect('renote.user', 'renoteUser');
 
 		this.queryService.generateVisibilityQuery(query, me);
-		if (me) this.queryService.generateMutedUserQuery(query, me);
-		if (me) this.queryService.generateBlockedUserQuery(query, me);
-		if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
+		if (me) {
+			this.queryService.generateMutedUserQuery(query, me);
+			this.queryService.generateBlockedUserQuery(query, me);
+			this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
+
+			const mutedChannelIds = await this.channelMutingService
+				.list({ requestUserId: me.id }, { idOnly: true })
+				.then(x => x.map(x => x.id));
+			if (mutedChannelIds.length > 0) {
+				query.andWhere(new Brackets(qb => {
+					qb.orWhere('note.renoteChannelId IS NULL')
+						.orWhere('note.renoteChannelId NOT IN (:...mutedChannelIds)', { mutedChannelIds });
+				}));
+			}
+		}
 
 		if (ps.withFiles) {
 			query.andWhere('note.fileIds != \'{}\'');
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index 7cb11cc1ebfc..b745167ec8a6 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 => {
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 87f9b322a602..d1bd27cf409e 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
@@ -14,6 +14,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 { ChannelMutingService } from '@/core/ChannelMutingService.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -84,6 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private idService: IdService,
 		private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
 		private queryService: QueryService,
+		private channelMutingService: ChannelMutingService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -188,6 +190,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		this.queryService.generateBlockedUserQuery(query, me);
 		this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
 
+		// -- ミュートされたチャンネルのリノート対策
+		const mutedChannelIds = await this.channelMutingService
+			.list({ requestUserId: me.id }, { idOnly: true })
+			.then(x => x.map(x => x.id));
+		if (mutedChannelIds.length > 0) {
+			query.andWhere(new Brackets(qb => {
+				qb.orWhere('note.renoteChannelId IS NULL')
+					.orWhere('note.renoteChannelId NOT IN (:...mutedChannelIds)', { mutedChannelIds });
+			}));
+		}
+
 		if (ps.includeMyRenotes === false) {
 			query.andWhere(new Brackets(qb => {
 				qb.orWhere('note.userId != :meId', { meId: me.id });
diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts
index 71f2782a5d9f..b8a32ba71c0e 100644
--- a/packages/backend/src/server/api/endpoints/roles/notes.ts
+++ b/packages/backend/src/server/api/endpoints/roles/notes.ts
@@ -5,6 +5,7 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import * as Redis from 'ioredis';
+import { Brackets } from 'typeorm';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { NotesRepository, RolesRepository } from '@/models/_.js';
 import { QueryService } from '@/core/QueryService.js';
@@ -12,6 +13,7 @@ import { DI } from '@/di-symbols.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { IdService } from '@/core/IdService.js';
 import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -68,6 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private noteEntityService: NoteEntityService,
 		private queryService: QueryService,
 		private fanoutTimelineService: FanoutTimelineService,
+		private channelMutingService: ChannelMutingService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -101,6 +104,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				.leftJoinAndSelect('reply.user', 'replyUser')
 				.leftJoinAndSelect('renote.user', 'renoteUser');
 
+			// -- ミュートされたチャンネル対策
+			const mutingChannelIds = await this.channelMutingService
+				.list({ requestUserId: me.id }, { idOnly: true })
+				.then(x => x.map(x => x.id));
+			if (mutingChannelIds.length > 0) {
+				query.andWhere(new Brackets(qb => {
+					qb.orWhere('note.channelId IS NULL');
+					qb.orWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
+				}));
+				query.andWhere(new Brackets(qb => {
+					qb.orWhere('note.renoteChannelId IS NULL');
+					qb.orWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
+				}));
+			}
+
 			this.queryService.generateVisibilityQuery(query, me);
 			this.queryService.generateMutedUserQuery(query, me);
 			this.queryService.generateBlockedUserQuery(query, me);
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index e9c334057e76..72561dfa92cd 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 { MiLocalUser } from '@/models/User.js';
 import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
 import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
 import { ApiError } from '@/server/api/error.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
 
 export const meta = {
 	tags: ['users', 'notes'],
@@ -77,12 +78,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 		@Inject(DI.notesRepository)
 		private notesRepository: NotesRepository,
-
 		private noteEntityService: NoteEntityService,
 		private queryService: QueryService,
 		private cacheService: CacheService,
 		private idService: IdService,
 		private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
+		private channelMutingService: ChannelMutingService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -163,6 +164,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		withFiles: boolean,
 		withRenotes: boolean,
 	}, me: MiLocalUser | null) {
+		const mutingChannelIds = me
+			? await this.channelMutingService
+				.list({ requestUserId: me.id }, { idOnly: true })
+				.then(x => x.map(x => x.id))
+			: [];
 		const isSelf = me && (me.id === ps.userId);
 
 		const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
@@ -175,14 +181,30 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			.leftJoinAndSelect('renote.user', 'renoteUser');
 
 		if (ps.withChannelNotes) {
-			if (!isSelf) query.andWhere(new Brackets(qb => {
-				qb.orWhere('note.channelId IS NULL');
-				qb.orWhere('channel.isSensitive = false');
+			query.andWhere(new Brackets(qb => {
+				if (mutingChannelIds.length > 0) {
+					qb.andWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds: mutingChannelIds });
+				}
+
+				if (!isSelf) {
+					qb.andWhere(new Brackets(qb2 => {
+						qb2.orWhere('note.channelId IS NULL');
+						qb2.orWhere('channel.isSensitive = false');
+					}));
+				}
 			}));
 		} else {
 			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 });
+			}));
+		}
+
 		this.queryService.generateVisibilityQuery(query, me);
 		if (me) {
 			this.queryService.generateMutedUserQuery(query, me, { id: ps.userId });
diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts
index 0fb5238c78e5..6c0e252742dd 100644
--- a/packages/backend/src/server/api/stream/Connection.ts
+++ b/packages/backend/src/server/api/stream/Connection.ts
@@ -12,8 +12,9 @@ import type { NotificationService } from '@/core/NotificationService.js';
 import { bindThis } from '@/decorators.js';
 import { CacheService } from '@/core/CacheService.js';
 import { MiFollowing, MiUserProfile } from '@/models/_.js';
-import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
+import type { GlobalEvents, StreamEventEmitter } from '@/core/GlobalEventService.js';
 import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
 import { isJsonObject } from '@/misc/json-value.js';
 import type { JsonObject, JsonValue } from '@/misc/json-value.js';
 import type { ChannelsService } from './ChannelsService.js';
@@ -37,6 +38,7 @@ export default class Connection {
 	public userProfile: MiUserProfile | null = null;
 	public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
 	public followingChannels: Set<string> = new Set();
+	public mutingChannels: Set<string> = new Set();
 	public userIdsWhoMeMuting: Set<string> = new Set();
 	public userIdsWhoBlockingMe: Set<string> = new Set();
 	public userIdsWhoMeMutingRenotes: Set<string> = new Set();
@@ -49,7 +51,7 @@ export default class Connection {
 		private notificationService: NotificationService,
 		private cacheService: CacheService,
 		private channelFollowingService: ChannelFollowingService,
-
+		private channelMutingService: ChannelMutingService,
 		user: MiUser | null | undefined,
 		token: MiAccessToken | null | undefined,
 	) {
@@ -60,10 +62,19 @@ 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,
+			mutingChannels,
+			userIdsWhoMeMuting,
+			userIdsWhoBlockingMe,
+			userIdsWhoMeMutingRenotes,
+		] = 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.channelMutingService.mutingChannelsCache.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),
@@ -71,6 +82,7 @@ export default class Connection {
 		this.userProfile = userProfile;
 		this.following = following;
 		this.followingChannels = followingChannels;
+		this.mutingChannels = mutingChannels;
 		this.userIdsWhoMeMuting = userIdsWhoMeMuting;
 		this.userIdsWhoBlockingMe = userIdsWhoBlockingMe;
 		this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes;
diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts
index 84cb552369fe..75cfab92401b 100644
--- a/packages/backend/src/server/api/stream/channel.ts
+++ b/packages/backend/src/server/api/stream/channel.ts
@@ -6,7 +6,8 @@
 import { bindThis } from '@/decorators.js';
 import { isInstanceMuted } from '@/misc/is-instance-muted.js';
 import { isUserRelated } from '@/misc/is-user-related.js';
-import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
+import { isChannelRelated } from '@/misc/is-channel-related.js';
 import type { Packed } from '@/misc/json-schema.js';
 import type { JsonObject, JsonValue } from '@/misc/json-value.js';
 import type Connection from './Connection.js';
@@ -55,6 +56,10 @@ export default abstract class Channel {
 		return this.connection.followingChannels;
 	}
 
+	protected get mutingChannels() {
+		return this.connection.mutingChannels;
+	}
+
 	protected get subscriber() {
 		return this.connection.subscriber;
 	}
@@ -74,6 +79,9 @@ export default abstract class Channel {
 		// 流れてきたNoteがリノートをミュートしてるユーザが行ったもの
 		if (isRenotePacked(note) && !isQuotePacked(note) && this.userIdsWhoMeMutingRenotes.has(note.user.id)) return true;
 
+		// 流れてきたNoteがミュートしているチャンネルと関わる
+		if (isChannelRelated(note, this.mutingChannels)) return true;
+
 		return false;
 	}
 
diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts
index 7108e0cd6e3a..32fa28a18953 100644
--- a/packages/backend/src/server/api/stream/channels/channel.ts
+++ b/packages/backend/src/server/api/stream/channels/channel.ts
@@ -8,6 +8,8 @@ import type { Packed } from '@/misc/json-schema.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import { isInstanceMuted } from '@/misc/is-instance-muted.js';
+import { isUserRelated } from '@/misc/is-user-related.js';
 import type { JsonObject } from '@/misc/json-value.js';
 import Channel, { type MiChannelService } from '../channel.js';
 
@@ -19,7 +21,6 @@ class ChannelChannel extends Channel {
 
 	constructor(
 		private noteEntityService: NoteEntityService,
-
 		id: string,
 		connection: Channel['connection'],
 	) {
@@ -54,6 +55,35 @@ class ChannelChannel extends Channel {
 		this.send('note', note);
 	}
 
+	/*
+	 * ミュートとブロックされてるを処理する
+	 */
+	protected override isNoteMutedOrBlocked(note: Packed<'Note'>): boolean {
+		// 流れてきたNoteがインスタンスミュートしたインスタンスが関わる
+		if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return true;
+
+		// 流れてきたNoteがミュートしているユーザーが関わる
+		if (isUserRelated(note, this.userIdsWhoMeMuting)) return true;
+		// 流れてきたNoteがブロックされているユーザーが関わる
+		if (isUserRelated(note, this.userIdsWhoBlockingMe)) return true;
+
+		// 流れてきたNoteがリノートをミュートしてるユーザが行ったもの
+		if (isRenotePacked(note) && !isQuotePacked(note) && this.userIdsWhoMeMutingRenotes.has(note.user.id)) return true;
+
+		// このソケットで見ているチャンネルがミュートされていたとしても、チャンネルを直接見ている以上は流すようにしたい
+		// ただし、他のミュートしているチャンネルは流さないようにもしたい
+		// ノート自体のチャンネルIDはonNoteでチェックしているので、ここではリノートのチャンネルIDをチェックする
+		if (
+			(note.renote) &&
+			(note.renote.channelId !== this.channelId) &&
+			(note.renote.channelId && this.mutingChannels.has(note.renote.channelId))
+		) {
+			return true;
+		}
+
+		return false;
+	}
+
 	@bindThis
 	public dispose() {
 		// Unsubscribe events
diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts
index 66644ed58cb2..825f71180df6 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -44,7 +44,10 @@ class HomeTimelineChannel extends Channel {
 		if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
 
 		if (note.channelId) {
-			if (!this.followingChannels.has(note.channelId)) return;
+			// そのチャンネルをフォローしていない
+			if (!this.followingChannels.has(note.channelId)) {
+				return;
+			}
 		} else {
 			// その投稿のユーザーをフォローしていなかったら弾く
 			if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index 568131149318..07baba9cc9dd 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -53,16 +53,25 @@ class HybridTimelineChannel extends Channel {
 
 		if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
 
-		// チャンネルの投稿ではなく、自分自身の投稿 または
-		// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
-		// チャンネルの投稿ではなく、全体公開のローカルの投稿 または
-		// フォローしているチャンネルの投稿 の場合だけ
-		if (!(
-			(note.channelId == null && isMe) ||
-			(note.channelId == null && Object.hasOwn(this.following, note.userId)) ||
-			(note.channelId == null && (note.user.host == null && note.visibility === 'public')) ||
-			(note.channelId != null && this.followingChannels.has(note.channelId))
-		)) return;
+		if (!note.channelId) {
+			// 以下の条件に該当するノートのみ後続処理に通す(ので、以下のif文は該当しないノートをすべて弾くようにする)
+			// - 自分自身の投稿
+			// - その投稿のユーザーをフォローしている
+			// - 全体公開のローカルの投稿
+			if (!(
+				isMe ||
+				Object.hasOwn(this.following, note.userId) ||
+				(note.user.host == null && note.visibility === 'public')
+			)) {
+				return;
+			}
+		} else {
+			// 以下の条件に該当するノートのみ後続処理に通す(ので、以下のif文は該当しないノートをすべて弾くようにする)
+			// - フォローしているチャンネルの投稿
+			if (!this.followingChannels.has(note.channelId)) {
+				return;
+			}
+		}
 
 		if (note.visibility === 'followers') {
 			if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts
index a544db955a07..c888941f1c32 100644
--- a/packages/backend/test/e2e/antennas.ts
+++ b/packages/backend/test/e2e/antennas.ts
@@ -69,6 +69,9 @@ describe('アンテナ', () => {
 	let userMutingAlice: User;
 	let userMutedByAlice: User;
 
+	let testChannel: misskey.entities.Channel;
+	let testMutedChannel: misskey.entities.Channel;
+
 	beforeAll(async () => {
 		root = await signup({ username: 'root' });
 		alice = await signup({ username: 'alice' });
@@ -120,6 +123,10 @@ describe('アンテナ', () => {
 		userMutedByAlice = await signup({ username: 'userMutedByAlice' });
 		await post(userMutedByAlice, { text: 'test' });
 		await api('mute/create', { userId: userMutedByAlice.id }, alice);
+
+		testChannel = (await api('channels/create', { name: 'test' }, root)).body;
+		testMutedChannel = (await api('channels/create', { name: 'test-muted' }, root)).body;
+		await api('channels/mute/create', { channelId: testMutedChannel.id }, alice);
 	}, 1000 * 60 * 10);
 
 	beforeEach(async () => {
@@ -593,6 +600,20 @@ describe('アンテナ', () => {
 					{ note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
 				],
 			},
+			{
+				label: 'チャンネルノートも含む',
+				parameters: () => ({ src: 'all' }),
+				posts: [
+					{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}`, channelId: testChannel.id }), included: true },
+				],
+			},
+			{
+				label: 'ミュートしてるチャンネルは含まない',
+				parameters: () => ({ src: 'all' }),
+				posts: [
+					{ note: (): Promise<Note> => post(bob, { text: `test ${keyword}`, channelId: testMutedChannel.id }) },
+				],
+			},
 		])('が取得できること($label)', async ({ parameters, posts }) => {
 			const antenna = await successfulApiCall({
 				endpoint: 'antennas/create',
diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts
index 319c8581f40b..3ee3d84e6599 100644
--- a/packages/backend/test/e2e/timelines.ts
+++ b/packages/backend/test/e2e/timelines.ts
@@ -3,13 +3,17 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
 // How to run:
 // pnpm jest -- e2e/timelines.ts
 
 import * as assert from 'assert';
 import { setTimeout } from 'node:timers/promises';
+import { entities } from 'misskey-js';
 import { Redis } from 'ioredis';
-import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js';
+import { afterEach, beforeAll } from '@jest/globals';
+import { api, initTestDb, post, randomString, sendEnvUpdateRequest, signup, uploadUrl, UserToken } from '../utils.js';
 import { loadConfig } from '@/config.js';
 
 function genHost() {
@@ -22,13 +26,78 @@ function waitForPushToTl() {
 
 let redisForTimelines: Redis;
 
+async function renote(noteId: string, user: UserToken): Promise<entities.Note> {
+	return await api('notes/create', { renoteId: noteId }, user).then(it => it.body.createdNote);
+}
+
+async function createChannel(name: string, user: UserToken): Promise<entities.ChannelsCreateResponse> {
+	return (await api('channels/create', { name }, user)).body;
+}
+
+async function followChannel(channelId: string, user: UserToken) {
+	return await api('channels/follow', { channelId }, user);
+}
+
+async function muteChannel(channelId: string, user: UserToken) {
+	await api('channels/mute/create', { channelId }, user);
+}
+
+async function createList(name: string, user: UserToken): Promise<entities.UsersListsCreateResponse> {
+	return (await api('users/lists/create', { name }, user)).body;
+}
+
+async function pushList(listId: string, pushUserIds: string[] = [], user: UserToken) {
+	for (const userId of pushUserIds) {
+		await api('users/lists/push', { listId, userId }, user);
+	}
+	await setTimeout(500);
+}
+
+async function createRole(name: string, user: UserToken): Promise<entities.AdminRolesCreateResponse> {
+	return (await api('admin/roles/create', {
+		name,
+		description: '',
+		color: '#000000',
+		iconUrl: '',
+		target: 'manual',
+		condFormula: {},
+		isPublic: true,
+		isModerator: false,
+		isAdministrator: false,
+		isExplorable: true,
+		asBadge: false,
+		canEditMembersByModerator: false,
+		displayOrder: 0,
+		policies: {},
+	}, user)).body;
+}
+
+async function assignRole(roleId: string, userId: string, user: UserToken) {
+	await api('admin/roles/assign', { userId, roleId }, user);
+}
+
 describe('Timelines', () => {
-	beforeAll(() => {
+	let root: UserToken;
+
+	beforeAll(async () => {
 		redisForTimelines = new Redis(loadConfig().redisForTimelines);
+		root = await signup({ username: 'root' });
+
+		// FTT無効の状態で見たいときはコメントアウトを外す
+		// await api('admin/update-meta', { enableFanoutTimeline: false }, root);
+		// await setTimeout(1000);
+	});
+
+	afterEach(async () => {
+		// テスト中に作ったノートをきれいにする。
+		// ユーザも作っているが、時間差で動く通知系処理などがあり、このタイミングで消すとエラー落ちするので消さない(ノートさえ消えていれば支障はない)
+		const db = await initTestDb(true);
+		await db.query('DELETE FROM "note"');
+		await db.query('DELETE FROM "channel"');
 	});
 
 	describe('Home TL', () => {
-		test.concurrent('自分の visibility: followers なノートが含まれる', async () => {
+		test('自分の visibility: followers なノートが含まれる', async () => {
 			const [alice] = await Promise.all([signup()]);
 
 			const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' });
@@ -41,7 +110,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
 		});
 
-		test.concurrent('フォローしているユーザーのノートが含まれる', async () => {
+		test('フォローしているユーザーのノートが含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -57,7 +126,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
+		test('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -74,7 +143,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => {
+		test('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -90,7 +159,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => {
+		test('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -107,14 +176,19 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => {
+		test('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
 			await api('following/update', { userId: bob.id, withReplies: true }, alice);
 			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
-			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] });
+			const bobNote = await post(bob, {
+				text: 'hi',
+				replyId: carolNote.id,
+				visibility: 'specified',
+				visibleUserIds: [carolNote.id],
+			});
 
 			await waitForPushToTl();
 
@@ -124,7 +198,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
+		test('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: carol.id }, bob);
@@ -142,7 +216,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => {
+		test('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -162,7 +236,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi');
 		});
 
-		test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => {
+		test('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -180,7 +254,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
 		});
 
-		test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => {
+		test('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -188,7 +262,12 @@ describe('Timelines', () => {
 			await api('following/update', { userId: bob.id, withReplies: true }, alice);
 			await setTimeout(1000);
 			const carolNote = await post(carol, { text: 'hi' });
-			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] });
+			const bobNote = await post(bob, {
+				text: 'hi',
+				replyId: carolNote.id,
+				visibility: 'specified',
+				visibleUserIds: [carolNote.id],
+			});
 
 			await waitForPushToTl();
 
@@ -198,7 +277,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
 		});
 
-		test.concurrent('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => {
+		test('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -214,7 +293,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		});
 
-		test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
+		test('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -230,7 +309,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('自分の他人への返信が含まれる', async () => {
+		test('自分の他人への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const bobNote = await post(bob, { text: 'hi' });
@@ -244,7 +323,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
 		});
 
-		test.concurrent('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => {
+		test('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -260,7 +339,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => {
+		test('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -278,7 +357,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => {
+		test('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -296,7 +375,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => {
+		test('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -310,7 +389,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
-		test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
+		test('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -327,7 +406,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
+		test('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -345,7 +424,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
+		test('フォローしているリモートユーザーのノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
 
 			await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
@@ -360,7 +439,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
+		test('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
 
 			await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
@@ -375,7 +454,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => {
+		test('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -399,7 +478,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false);
 		}, 1000 * 15);
 
-		test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
+		test('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
@@ -414,7 +493,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
-		test.concurrent('自分の visibility: specified なノートが含まれる', async () => {
+		test('自分の visibility: specified なノートが含まれる', async () => {
 			const [alice] = await Promise.all([signup()]);
 
 			const aliceNote = await post(alice, { text: 'hi', visibility: 'specified' });
@@ -427,7 +506,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
 		});
 
-		test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => {
+		test('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -442,7 +521,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
 		});
 
-		test.concurrent('フォローしていないユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれない', async () => {
+		test('フォローしていないユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれない', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] });
@@ -454,7 +533,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
-		test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定していない visibility: specified なノートが含まれない', async () => {
+		test('フォローしているユーザーの自身を visibleUserIds に指定していない visibility: specified なノートが含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -468,11 +547,16 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
-		test.concurrent('フォローしていないユーザーからの visibility: specified なノートに返信したときの自身のノートが含まれる', async () => {
+		test('フォローしていないユーザーからの visibility: specified なノートに返信したときの自身のノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] });
-			const aliceNote = await post(alice, { text: 'ok', visibility: 'specified', visibleUserIds: [bob.id], replyId: bobNote.id });
+			const aliceNote = await post(alice, {
+				text: 'ok',
+				visibility: 'specified',
+				visibleUserIds: [bob.id],
+				replyId: bobNote.id,
+			});
 
 			await waitForPushToTl();
 
@@ -483,7 +567,7 @@ describe('Timelines', () => {
 		});
 
 		/* TODO
-		test.concurrent('自身の visibility: specified なノートへのフォローしていないユーザーからの返信が含まれる', async () => {
+		test('自身の visibility: specified なノートへのフォローしていないユーザーからの返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const aliceNote = await post(alice, { text: 'hi', visibility: 'specified', visibleUserIds: [bob.id] });
@@ -499,20 +583,299 @@ describe('Timelines', () => {
 		*/
 
 		// ↑の挙動が理想だけど実装が面倒かも
-		test.concurrent('自身の visibility: specified なノートへのフォローしていないユーザーからの返信が含まれない', async () => {
+		test('自身の visibility: specified なノートへのフォローしていないユーザーからの返信が含まれない', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const aliceNote = await post(alice, { text: 'hi', visibility: 'specified', visibleUserIds: [bob.id] });
-			const bobNote = await post(bob, { text: 'ok', visibility: 'specified', visibleUserIds: [alice.id], replyId: aliceNote.id });
+			const bobNote = await post(bob, {
+				text: 'ok',
+				visibility: 'specified',
+				visibleUserIds: [alice.id],
+				replyId: aliceNote.id,
+			});
 
 			await waitForPushToTl();
 
 			const res = await api('notes/timeline', { limit: 100 }, alice);
 
-			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+		});
+
+		describe('Channel', () => {
+			test('チャンネル未フォロー + ユーザ未フォロー = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネルフォロー + ユーザ未フォロー = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			});
+
+			test('チャンネル未フォロー + ユーザフォロー = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネルフォロー + ユーザフォロー = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			});
+
+			test('チャンネル未フォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネルフォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネル未フォロー + ユーザフォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネルフォロー + ユーザフォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + ユーザ未フォロー = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + ユーザ未フォロー = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + ユーザフォロー = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + ユーザフォロー = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + ユーザフォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + ユーザフォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
 		});
 
-		test.concurrent('FTT: ローカルユーザーの HTL にはプッシュされる', async () => {
+		test('FTT: ローカルユーザーの HTL にはプッシュされる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', {
@@ -534,7 +897,7 @@ describe('Timelines', () => {
 			assert.strictEqual(bobHTL.includes(carolNote.id), false);
 		});
 
-		test.concurrent('FTT: リモートユーザーの HTL にはプッシュされない', async () => {
+		test('FTT: リモートユーザーの HTL にはプッシュされない', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
 
 			await api('following/create', {
@@ -552,7 +915,7 @@ describe('Timelines', () => {
 	});
 
 	describe('Local TL', () => {
-		test.concurrent('visibility: home なノートが含まれない', async () => {
+		test('visibility: home なノートが含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			const carolNote = await post(carol, { text: 'hi', visibility: 'home' });
@@ -566,7 +929,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('他人の他人への返信が含まれない', async () => {
+		test('他人の他人への返信が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			const carolNote = await post(carol, { text: 'hi' });
@@ -580,7 +943,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
 		});
 
-		test.concurrent('他人のその人自身への返信が含まれる', async () => {
+		test('他人のその人自身への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const bobNote1 = await post(bob, { text: 'hi' });
@@ -594,7 +957,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		});
 
-		test.concurrent('チャンネル投稿が含まれない', async () => {
+		test('チャンネル投稿が含まれない', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
@@ -607,7 +970,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
-		test.concurrent('リモートユーザーのノートが含まれない', async () => {
+		test('リモートユーザーのノートが含まれない', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
 
 			const bobNote = await post(bob, { text: 'hi' });
@@ -620,7 +983,7 @@ describe('Timelines', () => {
 		});
 
 		// 含まれても良いと思うけど実装が面倒なので含まれない
-		test.concurrent('フォローしているユーザーの visibility: home なノートが含まれない', async () => {
+		test('フォローしているユーザーの visibility: home なノートが含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: carol.id }, alice);
@@ -636,7 +999,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('ミュートしているユーザーのノートが含まれない', async () => {
+		test('ミュートしているユーザーのノートが含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('mute/create', { userId: carol.id }, alice);
@@ -652,7 +1015,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
+		test('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -669,7 +1032,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
+		test('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -687,7 +1050,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
+		test('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -703,7 +1066,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => {
+		test('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await setTimeout(1000);
@@ -718,7 +1081,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => {
+		test('[withReplies: true] 他人の他人への返信が含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			const carolNote = await post(carol, { text: 'hi' });
@@ -731,7 +1094,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
+		test('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png');
@@ -745,10 +1108,284 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		}, 1000 * 10);
+
+		describe('Channel', () => {
+			test('チャンネル未フォロー + ユーザ未フォロー = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネルフォロー + ユーザ未フォロー = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネル未フォロー + ユーザフォロー = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネルフォロー + ユーザフォロー = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネル未フォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネルフォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネル未フォロー + ユーザフォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネルフォロー + ユーザフォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + ユーザ未フォロー = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + ユーザ未フォロー = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + ユーザフォロー = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + ユーザフォロー = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + ユーザフォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + ユーザフォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/local-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+		});
 	});
 
 	describe('Social TL', () => {
-		test.concurrent('ローカルユーザーのノートが含まれる', async () => {
+		test('ローカルユーザーのノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const bobNote = await post(bob, { text: 'hi' });
@@ -760,7 +1397,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('ローカルユーザーの visibility: home なノートが含まれない', async () => {
+		test('ローカルユーザーの visibility: home なノートが含まれない', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
@@ -772,7 +1409,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
-		test.concurrent('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => {
+		test('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -786,7 +1423,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
+		test('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -802,7 +1439,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
+		test('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: carol.id }, bob);
@@ -820,7 +1457,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
 		});
 
-		test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => {
+		test('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -840,7 +1477,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id)?.text, 'hi');
 		});
 
-		test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => {
+		test('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -858,7 +1495,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
 		});
 
-		test.concurrent('他人の他人への返信が含まれない', async () => {
+		test('他人の他人への返信が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			const carolNote = await post(carol, { text: 'hi' });
@@ -872,7 +1509,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === carolNote.id), true);
 		});
 
-		test.concurrent('リモートユーザーのノートが含まれない', async () => {
+		test('リモートユーザーのノートが含まれない', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
 
 			const bobNote = await post(bob, { text: 'hi' });
@@ -884,7 +1521,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
-		test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
+		test('フォローしているリモートユーザーのノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
 
 			await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
@@ -899,7 +1536,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
+		test('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
 
 			await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
@@ -914,7 +1551,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => {
+		test('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await setTimeout(1000);
@@ -929,7 +1566,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => {
+		test('[withReplies: true] 他人の他人への返信が含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			const carolNote = await post(carol, { text: 'hi' });
@@ -942,7 +1579,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
+		test('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png');
@@ -956,71 +1593,345 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false);
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		}, 1000 * 10);
-	});
 
-	describe('User List TL', () => {
-		test.concurrent('リスインしているフォローしていないユーザーのノートが含まれる', async () => {
-			const [alice, bob] = await Promise.all([signup(), signup()]);
+		describe('Channel', () => {
+			test('チャンネル未フォロー + ユーザ未フォロー = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([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 setTimeout(1000);
-			const bobNote = await post(bob, { text: 'hi' });
+				const channel = await createChannel('channel', bob);
 
-			await waitForPushToTl();
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
 
-			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
+				await waitForPushToTl();
 
-			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
-		});
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
 
-		test.concurrent('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => {
-			const [alice, bob] = await Promise.all([signup(), signup()]);
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
 
-			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 setTimeout(1000);
-			const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
+			test('チャンネルフォロー + ユーザ未フォロー = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
 
-			await waitForPushToTl();
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
 
-			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
 
-			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
-		});
+				await waitForPushToTl();
 
-		test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
-			const [alice, bob] = await Promise.all([signup(), signup()]);
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
 
-			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 setTimeout(1000);
-			const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			});
 
-			await waitForPushToTl();
+			test('チャンネル未フォロー + ユーザフォロー = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
 
-			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
+				const channel = await createChannel('channel', bob);
 
-			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
-		});
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
 
-		test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
-			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
+				await waitForPushToTl();
 
-			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 setTimeout(1000);
-			const carolNote = await post(carol, { text: 'hi' });
-			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
 
-			await waitForPushToTl();
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
 
-			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
+			test('チャンネルフォロー + ユーザフォロー = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
 
-			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			});
+
+			test('チャンネル未フォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネルフォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネル未フォロー + ユーザフォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネルフォロー + ユーザフォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + ユーザ未フォロー = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + ユーザ未フォロー = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + ユーザフォロー = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + ユーザフォロー = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + ユーザフォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + ユーザフォロー + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+				await api('following/create', { userId: bob.id }, alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+		});
+	});
+
+	describe('User List TL', () => {
+		test('リスインしているフォローしていないユーザーのノートが含まれる', async () => {
+			const [alice, bob] = await Promise.all([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 setTimeout(1000);
+			const bobNote = await post(bob, { text: 'hi' });
+
+			await waitForPushToTl();
+
+			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
+
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => {
+		test('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => {
+			const [alice, bob] = await Promise.all([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 setTimeout(1000);
+			const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
+
+			await waitForPushToTl();
+
+			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
+
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
+		});
+
+		test('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
+			const [alice, bob] = await Promise.all([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 setTimeout(1000);
+			const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
+
+			await waitForPushToTl();
+
+			const res = await api('notes/user-list-timeline', { listId: list.id }, alice);
+
+			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
+		});
+
+		test('リスインしているフォローしていないユーザーの他人への返信が含まれない', 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 setTimeout(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 => note.id === bobNote.id), false);
+		});
+
+		test('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
@@ -1037,7 +1948,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		});
 
-		test.concurrent('withReplies: false でリスインしているフォローしていないユーザーからの自分への返信が含まれる', async () => {
+		test('withReplies: false でリスインしているフォローしていないユーザーからの自分への返信が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
@@ -1054,7 +1965,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
+		test('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);
@@ -1071,7 +1982,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
-		test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => {
+		test('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', 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);
@@ -1088,7 +1999,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => {
+		test('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -1104,7 +2015,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => {
+		test('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -1121,7 +2032,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
 		});
 
-		test.concurrent('リスインしている自分の visibility: followers なノートが含まれる', async () => {
+		test('リスインしている自分の visibility: followers なノートが含まれる', async () => {
 			const [alice] = await Promise.all([signup(), signup()]);
 
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
@@ -1137,7 +2048,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
 		});
 
-		test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => {
+		test('リスインしているユーザーのチャンネルノートが含まれない', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
@@ -1153,7 +2064,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
-		test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => {
+		test('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
@@ -1170,7 +2081,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		}, 1000 * 10);
 
-		test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => {
+		test('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
@@ -1186,7 +2097,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
 		});
 
-		test.concurrent('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', async () => {
+		test('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', 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);
@@ -1201,10 +2112,316 @@ describe('Timelines', () => {
 
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
+
+		describe('Channel', () => {
+			test('チャンネル未フォロー + リスインしてない = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネルフォロー + リスインしてない = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネル未フォロー + リスインしてる = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+				await pushList(list.id, [bob.id], alice);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネルフォロー + リスインしてる = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+				await pushList(list.id, [bob.id], alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネル未フォロー + リスインしてない + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネルフォロー + リスインしてない + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネル未フォロー + リスインしてる + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+				await pushList(list.id, [bob.id], alice);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('チャンネルフォロー + リスインしてる + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+				await pushList(list.id, [bob.id], alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + リスインしてない = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + リスインしてない = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + リスインしてる = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+				await pushList(list.id, [bob.id], alice);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + リスインしてる = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+				await pushList(list.id, [bob.id], alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + リスインしてない + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + リスインしてない + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネル未フォロー + リスインしてる + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+				await pushList(list.id, [bob.id], alice);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネルフォロー + リスインしてる + チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const list = await createList('list', alice);
+				await pushList(list.id, [bob.id], alice);
+
+				const channel = await createChannel('channel', bob);
+				await followChannel(channel.id, alice);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+		});
 	});
 
 	describe('User TL', () => {
-		test.concurrent('ノートが含まれる', async () => {
+		test('ノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const bobNote = await post(bob, { text: 'hi' });
@@ -1216,7 +2433,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
+		test('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
@@ -1228,7 +2445,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
-		test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
+		test('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('following/create', { userId: bob.id }, alice);
@@ -1243,7 +2460,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi');
 		});
 
-		test.concurrent('自身の visibility: followers なノートが含まれる', async () => {
+		test('自身の visibility: followers なノートが含まれる', async () => {
 			const [alice] = await Promise.all([signup()]);
 
 			const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' });
@@ -1256,7 +2473,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi');
 		});
 
-		test.concurrent('チャンネル投稿が含まれない', async () => {
+		test('チャンネル投稿が含まれない', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
@@ -1269,7 +2486,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
-		test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => {
+		test('[withReplies: false] 他人への返信が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			const carolNote = await post(carol, { text: 'hi' });
@@ -1284,7 +2501,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false);
 		});
 
-		test.concurrent('[withReplies: true] 他人への返信が含まれる', async () => {
+		test('[withReplies: true] 他人への返信が含まれる', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			const carolNote = await post(carol, { text: 'hi' });
@@ -1299,7 +2516,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		});
 
-		test.concurrent('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => {
+		test('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			const carolNote = await post(carol, { text: 'hi' });
@@ -1314,7 +2531,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false);
 		});
 
-		test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
+		test('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png');
@@ -1329,7 +2546,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
 		}, 1000 * 10);
 
-		test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => {
+		test('[withChannelNotes: true] チャンネル投稿が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
@@ -1342,7 +2559,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('[withChannelNotes: true] 他人が取得した場合センシティブチャンネル投稿が含まれない', async () => {
+		test('[withChannelNotes: true] 他人が取得した場合センシティブチャンネル投稿が含まれない', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const channel = await api('channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body);
@@ -1355,7 +2572,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
-		test.concurrent('[withChannelNotes: true] 自分が取得した場合センシティブチャンネル投稿が含まれる', async () => {
+		test('[withChannelNotes: true] 自分が取得した場合センシティブチャンネル投稿が含まれる', async () => {
 			const [bob] = await Promise.all([signup()]);
 
 			const channel = await api('channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body);
@@ -1368,7 +2585,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
 		});
 
-		test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => {
+		test('ミュートしているユーザーに関連する投稿が含まれない', async () => {
 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 
 			await api('mute/create', { userId: carol.id }, alice);
@@ -1383,7 +2600,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
 		});
 
-		test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => {
+		test('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			await api('mute/create', { userId: bob.id }, alice);
@@ -1401,7 +2618,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true);
 		});
 
-		test.concurrent('自身の visibility: specified なノートが含まれる', async () => {
+		test('自身の visibility: specified なノートが含まれる', async () => {
 			const [alice] = await Promise.all([signup()]);
 
 			const aliceNote = await post(alice, { text: 'hi', visibility: 'specified' });
@@ -1413,7 +2630,7 @@ describe('Timelines', () => {
 			assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
 		});
 
-		test.concurrent('visibleUserIds に指定されてない visibility: specified なノートが含まれない', async () => {
+		test('visibleUserIds に指定されてない visibility: specified なノートが含まれない', async () => {
 			const [alice, bob] = await Promise.all([signup(), signup()]);
 
 			const bobNote = await post(bob, { text: 'hi', visibility: 'specified' });
@@ -1426,7 +2643,7 @@ describe('Timelines', () => {
 		});
 
 		/** @see https://github.com/misskey-dev/misskey/issues/14000 */
-		test.concurrent('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId による絞り込みが正しく動作する', async () => {
+		test('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId による絞り込みが正しく動作する', async () => {
 			const alice = await signup();
 			const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' });
 			const note1 = await post(alice, { text: '1' });
@@ -1438,7 +2655,7 @@ describe('Timelines', () => {
 			assert.deepStrictEqual(res.body, [note1, note2, note3]);
 		});
 
-		test.concurrent('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId と untilId による絞り込みが正しく動作する', async () => {
+		test('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId と untilId による絞り込みが正しく動作する', async () => {
 			const alice = await signup();
 			const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' });
 			const note1 = await post(alice, { text: '1' });
@@ -1451,8 +2668,458 @@ describe('Timelines', () => {
 			const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id, untilId: noteUntil.id });
 			assert.deepStrictEqual(res.body, [note3, note2, note1]);
 		});
+
+		describe('Channel', () => {
+			test('チャンネルミュートなし = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			});
+
+			test('チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネルミュートなし = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+			});
+
+			test('[チャンネル外リノート] チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+		});
 	});
 
+	describe('Role TL', () => {
+		test('ロールにアサインされているユーザーのノートが含まれる', async () => {
+			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
+
+			const role = await createRole('role', root);
+			await assignRole(role.id, alice.id, root);
+			await assignRole(role.id, bob.id, root);
+			await assignRole(role.id, carol.id, root);
+
+			const bobNote = await post(bob, { text: 'hi' });
+			const carolNote = await post(carol, { text: 'hi' });
+
+			await waitForPushToTl();
+
+			const res = await api('roles/notes', { roleId: role.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
+		});
+
+		test('ロールにアサインされていないユーザーのノートは含まれない', async () => {
+			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
+
+			const role = await createRole('role', root);
+			await assignRole(role.id, alice.id, root);
+			await assignRole(role.id, bob.id, root);
+
+			const bobNote = await post(bob, { text: 'hi' });
+			const carolNote = await post(carol, { text: 'hi' });
+
+			await waitForPushToTl();
+
+			const res = await api('roles/notes', { roleId: role.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+		});
+
+		test('自分の他人への返信が含まれる', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			const role = await createRole('role', root);
+			await assignRole(role.id, alice.id, root);
+			await assignRole(role.id, bob.id, root);
+
+			const bobNote = await post(bob, { text: 'hi' });
+			const aliceNote = await post(alice, { text: 'hi', replyId: bobNote.id });
+
+			await waitForPushToTl();
+
+			const res = await api('roles/notes', { roleId: role.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
+		});
+
+		test('他人の自分への返信が含まれる', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			const role = await createRole('role', root);
+			await assignRole(role.id, alice.id, root);
+			await assignRole(role.id, bob.id, root);
+
+			const aliceNote = await post(alice, { text: 'hi' });
+			const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
+
+			await waitForPushToTl();
+
+			const res = await api('roles/notes', { roleId: role.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
+		});
+
+		test('ミュートしているユーザのノートは含まれない', async () => {
+			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
+
+			const role = await createRole('role', root);
+			await assignRole(role.id, alice.id, root);
+			await assignRole(role.id, bob.id, root);
+			await assignRole(role.id, carol.id, root);
+
+			await api('mute/create', { userId: carol.id }, alice);
+
+			const bobNote = await post(bob, { text: 'hi' });
+			const carolNote = await post(carol, { text: 'hi' });
+
+			await waitForPushToTl();
+
+			const res = await api('roles/notes', { roleId: role.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+		});
+
+		test('こちらをブロックしているユーザのノートは含まれない', async () => {
+			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
+
+			const role = await createRole('role', root);
+			await assignRole(role.id, alice.id, root);
+			await assignRole(role.id, bob.id, root);
+			await assignRole(role.id, carol.id, root);
+
+			await api('blocking/create', { userId: alice.id }, carol);
+
+			const bobNote = await post(bob, { text: 'hi' });
+			const carolNote = await post(carol, { text: 'hi' });
+
+			await waitForPushToTl();
+
+			const res = await api('roles/notes', { roleId: role.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+		});
+
+		describe('Channel', () => {
+			test('チャンネルミュートなし = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const role = await createRole('role', root);
+				await assignRole(role.id, alice.id, root);
+				await assignRole(role.id, bob.id, root);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('roles/notes', { roleId: role.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			});
+
+			test('チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const role = await createRole('role', root);
+				await assignRole(role.id, alice.id, root);
+				await assignRole(role.id, bob.id, root);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+				await waitForPushToTl();
+
+				const res = await api('roles/notes', { roleId: role.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+			});
+
+			test('[チャンネル外リノート] チャンネルミュートなし = TLに流れる', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const role = await createRole('role', root);
+				await assignRole(role.id, alice.id, root);
+				await assignRole(role.id, bob.id, root);
+
+				const channel = await createChannel('channel', bob);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('roles/notes', { roleId: role.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+			});
+
+			test('[チャンネル外リノート] チャンネルミュート = TLに流れない', async () => {
+				const [alice, bob] = await Promise.all([signup(), signup()]);
+
+				const role = await createRole('role', root);
+				await assignRole(role.id, alice.id, root);
+				await assignRole(role.id, bob.id, root);
+
+				const channel = await createChannel('channel', bob);
+				await muteChannel(channel.id, alice);
+
+				const aliceNote = await post(alice, { text: 'hi' });
+				const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+				const bobRenote = await renote(bobNote.id, bob);
+
+				await waitForPushToTl();
+
+				const res = await api('roles/notes', { roleId: role.id }, alice);
+
+				assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+			});
+		});
+	});
+
+	describe('Channel TL', () => {
+		test('閲覧中チャンネルのノートが含まれる', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			const channel = await createChannel('channel', bob);
+
+			const aliceNote = await post(alice, { text: 'hi' });
+			const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+			await waitForPushToTl();
+
+			const res = await api('channels/timeline', { channelId: channel.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false);
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+		});
+
+		test('閲覧中チャンネルとは別チャンネルのノートは含まれない', async() => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			const channel = await createChannel('channel', bob);
+			const channel2 = await createChannel('channel', bob);
+
+			const aliceNote = await post(alice, { text: 'hi' });
+			const bobNote = await post(bob, { text: 'ok', channelId: channel2.id });
+
+			await waitForPushToTl();
+
+			const res = await api('channels/timeline', { channelId: channel.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false);
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+		});
+
+		test('閲覧中チャンネルのノートにリノートが含まれる', async() => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			const channel = await createChannel('channel', bob);
+
+			const aliceNote = await post(alice, { text: 'hi' });
+			const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+			const bobRenote = await post(bob, { channelId: channel.id, renoteId: bobNote.id });
+
+			await waitForPushToTl();
+
+			const res = await api('channels/timeline', { channelId: channel.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+		});
+
+		test('閲覧中チャンネルとは別チャンネルからのリノートが含まれる', async() => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			const channel = await createChannel('channel', bob);
+			const channel2 = await createChannel('channel', bob);
+
+			const aliceNote = await post(alice, { text: 'hi' });
+			const bobNote = await post(bob, { text: 'ok', channelId: channel2.id });
+			const bobRenote = await post(bob, { channelId: channel.id, renoteId: bobNote.id });
+
+			await waitForPushToTl();
+
+			const res = await api('channels/timeline', { channelId: channel.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+		});
+
+		test('閲覧中チャンネルに自分の他人への返信が含まれる', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			const channel = await createChannel('channel', bob);
+
+			const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+			const aliceNote = await post(alice, { text: 'hi', replyId: bobNote.id, channelId: channel.id });
+
+			await waitForPushToTl();
+
+			const res = await api('channels/timeline', { channelId: channel.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
+		});
+
+		test('閲覧中チャンネルに他人の自分への返信が含まれる', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			const channel = await createChannel('channel', bob);
+
+			const aliceNote = await post(alice, { text: 'hi', channelId: channel.id });
+			const bobNote = await post(bob, { text: 'ok', replyId: aliceNote.id, channelId: channel.id });
+
+			await waitForPushToTl();
+
+			const res = await api('channels/timeline', { channelId: channel.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+		});
+
+		test('閲覧中チャンネルにミュートしているユーザのノートは含まれない', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			await api('mute/create', { userId: bob.id }, alice);
+
+			const channel = await createChannel('channel', bob);
+
+			const aliceNote = await post(alice, { text: 'hi' });
+			const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+			await waitForPushToTl();
+
+			const res = await api('channels/timeline', { channelId: channel.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false);
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+		});
+
+		test('閲覧中チャンネルにこちらをブロックしているユーザのノートは含まれない', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			await api('blocking/create', { userId: alice.id }, bob);
+
+			const channel = await createChannel('channel', bob);
+
+			const aliceNote = await post(alice, { text: 'hi' });
+			const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+			await waitForPushToTl();
+
+			const res = await api('channels/timeline', { channelId: channel.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false);
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+		});
+
+		test('閲覧中チャンネルをミュートしていてもノートが含まれる', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			const channel = await createChannel('channel', bob);
+			await muteChannel(channel.id, alice);
+
+			const aliceNote = await post(alice, { text: 'hi' });
+			const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+
+			await waitForPushToTl();
+
+			const res = await api('channels/timeline', { channelId: channel.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false);
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+		});
+
+		test('閲覧中チャンネルをミュートしていてもリノートが含まれる', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			const channel = await createChannel('channel', bob);
+			await muteChannel(channel.id, alice);
+
+			const aliceNote = await post(alice, { text: 'hi' });
+			const bobNote = await post(bob, { text: 'ok', channelId: channel.id });
+			const bobRenote = await post(bob, { channelId: channel.id, renoteId: bobNote.id });
+
+			await waitForPushToTl();
+
+			const res = await api('channels/timeline', { channelId: channel.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true);
+		});
+
+		test('閲覧中チャンネルとは別チャンネルをミュートしているとき、そのチャンネルからのリノートは含まれない', async() => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			const channel = await createChannel('channel', bob);
+			const channel2 = await createChannel('channel', bob);
+			await muteChannel(channel2.id, alice);
+
+			const aliceNote = await post(alice, { text: 'hi' });
+			const bobNote = await post(bob, { text: 'ok', channelId: channel2.id });
+			const bobRenote = await post(bob, { channelId: channel.id, renoteId: bobNote.id });
+
+			await waitForPushToTl();
+
+			const res = await api('channels/timeline', { channelId: channel.id }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), false);
+		});
+	});
 	// TODO: リノートミュート済みユーザーのテスト
 	// TODO: ページネーションのテスト
 });
diff --git a/packages/backend/test/jest.setup.unit.cjs b/packages/backend/test/jest.setup.unit.cjs
new file mode 100644
index 000000000000..dd879c81c882
--- /dev/null
+++ b/packages/backend/test/jest.setup.unit.cjs
@@ -0,0 +1,10 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+module.exports = async () => {
+	// DBはUTC(っぽい)ので、テスト側も合わせておく
+	process.env.TZ = 'UTC';
+	process.env.NODE_ENV = 'test';
+};
diff --git a/packages/backend/test/unit/ChannelFollowingService.ts b/packages/backend/test/unit/ChannelFollowingService.ts
new file mode 100644
index 000000000000..9ca4fe0842c9
--- /dev/null
+++ b/packages/backend/test/unit/ChannelFollowingService.ts
@@ -0,0 +1,235 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable */
+
+import { afterEach, beforeEach, describe, expect } from '@jest/globals';
+import { Test, TestingModule } from '@nestjs/testing';
+import { GlobalModule } from '@/GlobalModule.js';
+import { CoreModule } from '@/core/CoreModule.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { IdService } from '@/core/IdService.js';
+import {
+	type ChannelFollowingsRepository,
+	ChannelsRepository,
+	DriveFilesRepository,
+	MiChannel,
+	MiChannelFollowing,
+	MiDriveFile,
+	MiUser,
+	UserProfilesRepository,
+	UsersRepository,
+} from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ChannelFollowingService } from "@/core/ChannelFollowingService.js";
+import { MiLocalUser } from "@/models/User.js";
+
+describe('ChannelFollowingService', () => {
+	let app: TestingModule;
+	let service: ChannelFollowingService;
+	let channelsRepository: ChannelsRepository;
+	let channelFollowingsRepository: ChannelFollowingsRepository;
+	let usersRepository: UsersRepository;
+	let userProfilesRepository: UserProfilesRepository;
+	let driveFilesRepository: DriveFilesRepository;
+	let idService: IdService;
+
+	let alice: MiLocalUser;
+	let bob: MiLocalUser;
+	let channel1: MiChannel;
+	let channel2: MiChannel;
+	let channel3: MiChannel;
+	let driveFile1: MiDriveFile;
+	let driveFile2: MiDriveFile;
+
+	async function createUser(data: Partial<MiUser> = {}) {
+		const user = await usersRepository
+			.insert({
+				id: idService.gen(),
+				username: 'username',
+				usernameLower: 'username',
+				...data,
+			})
+			.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+		await userProfilesRepository.insert({
+			userId: user.id,
+		});
+
+		return user;
+	}
+
+	async function createChannel(data: Partial<MiChannel> = {}) {
+		return await channelsRepository
+			.insert({
+				id: idService.gen(),
+				...data,
+			})
+			.then(x => channelsRepository.findOneByOrFail(x.identifiers[0]));
+	}
+
+	async function createChannelFollowing(data: Partial<MiChannelFollowing> = {}) {
+		return await channelFollowingsRepository
+			.insert({
+				id: idService.gen(),
+				...data,
+			})
+			.then(x => channelFollowingsRepository.findOneByOrFail(x.identifiers[0]));
+	}
+
+	async function fetchChannelFollowing() {
+		return await channelFollowingsRepository.findBy({});
+	}
+
+	async function createDriveFile(data: Partial<MiDriveFile> = {}) {
+		return await driveFilesRepository
+			.insert({
+				id: idService.gen(),
+				md5: 'md5',
+				name: 'name',
+				size: 0,
+				type: 'type',
+				storedInternal: false,
+				url: 'url',
+				...data,
+			})
+			.then(x => driveFilesRepository.findOneByOrFail(x.identifiers[0]));
+	}
+
+	beforeAll(async () => {
+		app = await Test.createTestingModule({
+			imports: [
+				GlobalModule,
+				CoreModule,
+			],
+			providers: [
+				GlobalEventService,
+				IdService,
+				ChannelFollowingService,
+			],
+		}).compile();
+
+		app.enableShutdownHooks();
+
+		service = app.get<ChannelFollowingService>(ChannelFollowingService);
+		idService = app.get<IdService>(IdService);
+		channelsRepository = app.get<ChannelsRepository>(DI.channelsRepository);
+		channelFollowingsRepository = app.get<ChannelFollowingsRepository>(DI.channelFollowingsRepository);
+		usersRepository = app.get<UsersRepository>(DI.usersRepository);
+		userProfilesRepository = app.get<UserProfilesRepository>(DI.userProfilesRepository);
+		driveFilesRepository = app.get<DriveFilesRepository>(DI.driveFilesRepository);
+	});
+
+	afterAll(async () => {
+		await app.close();
+	});
+
+	beforeEach(async () => {
+		alice = { ...await createUser({ username: 'alice' }), host: null, uri: null };
+		bob = { ...await createUser({ username: 'bob' }), host: null, uri: null };
+		driveFile1 = await createDriveFile();
+		driveFile2 = await createDriveFile();
+		channel1 = await createChannel({ name: 'channel1', userId: alice.id, bannerId: driveFile1.id });
+		channel2 = await createChannel({ name: 'channel2', userId: alice.id, bannerId: driveFile2.id });
+		channel3 = await createChannel({ name: 'channel3', userId: alice.id, bannerId: driveFile2.id });
+	});
+
+	afterEach(async () => {
+		await channelFollowingsRepository.delete({});
+		await channelsRepository.delete({});
+		await userProfilesRepository.delete({});
+		await usersRepository.delete({});
+	});
+
+	describe('list', () => {
+		test('default', async () => {
+			await createChannelFollowing({ followerId: alice.id, followeeId: channel1.id });
+			await createChannelFollowing({ followerId: alice.id, followeeId: channel2.id });
+			await createChannelFollowing({ followerId: bob.id, followeeId: channel3.id });
+
+			const followings = await service.list({ requestUserId: alice.id });
+
+			expect(followings).toHaveLength(2);
+			expect(followings[0].id).toBe(channel1.id);
+			expect(followings[0].userId).toBe(alice.id);
+			expect(followings[0].user).toBeFalsy();
+			expect(followings[0].bannerId).toBe(driveFile1.id);
+			expect(followings[0].banner).toBeFalsy();
+			expect(followings[1].id).toBe(channel2.id);
+			expect(followings[1].userId).toBe(alice.id);
+			expect(followings[1].user).toBeFalsy();
+			expect(followings[1].bannerId).toBe(driveFile2.id);
+			expect(followings[1].banner).toBeFalsy();
+		});
+
+		test('idOnly', async () => {
+			await createChannelFollowing({ followerId: alice.id, followeeId: channel1.id });
+			await createChannelFollowing({ followerId: alice.id, followeeId: channel2.id });
+			await createChannelFollowing({ followerId: bob.id, followeeId: channel3.id });
+
+			const followings = await service.list({ requestUserId: alice.id }, { idOnly: true });
+
+			expect(followings).toHaveLength(2);
+			expect(followings[0].id).toBe(channel1.id);
+			expect(followings[1].id).toBe(channel2.id);
+		});
+
+		test('joinUser', async () => {
+			await createChannelFollowing({ followerId: alice.id, followeeId: channel1.id });
+			await createChannelFollowing({ followerId: alice.id, followeeId: channel2.id });
+			await createChannelFollowing({ followerId: bob.id, followeeId: channel3.id });
+
+			const followings = await service.list({ requestUserId: alice.id }, { joinUser: true });
+
+			expect(followings).toHaveLength(2);
+			expect(followings[0].id).toBe(channel1.id);
+			expect(followings[0].user).toEqual(alice);
+			expect(followings[0].banner).toBeFalsy();
+			expect(followings[1].id).toBe(channel2.id);
+			expect(followings[1].user).toEqual(alice);
+			expect(followings[1].banner).toBeFalsy();
+		});
+
+		test('joinBannerFile', async () => {
+			await createChannelFollowing({ followerId: alice.id, followeeId: channel1.id });
+			await createChannelFollowing({ followerId: alice.id, followeeId: channel2.id });
+			await createChannelFollowing({ followerId: bob.id, followeeId: channel3.id });
+
+			const followings = await service.list({ requestUserId: alice.id }, { joinBannerFile: true });
+
+			expect(followings).toHaveLength(2);
+			expect(followings[0].id).toBe(channel1.id);
+			expect(followings[0].user).toBeFalsy();
+			expect(followings[0].banner).toEqual(driveFile1);
+			expect(followings[1].id).toBe(channel2.id);
+			expect(followings[1].user).toBeFalsy();
+			expect(followings[1].banner).toEqual(driveFile2);
+		});
+	});
+
+	describe('follow', () => {
+		test('default', async () => {
+			await service.follow(alice, channel1);
+
+			const followings = await fetchChannelFollowing();
+
+			expect(followings).toHaveLength(1);
+			expect(followings[0].followeeId).toBe(channel1.id);
+			expect(followings[0].followerId).toBe(alice.id);
+		});
+	});
+
+	describe('unfollow', () => {
+		test('default', async () => {
+			await createChannelFollowing({ followerId: alice.id, followeeId: channel1.id });
+
+			await service.unfollow(alice, channel1);
+
+			const followings = await fetchChannelFollowing();
+
+			expect(followings).toHaveLength(0);
+		});
+	});
+});
diff --git a/packages/backend/test/unit/ChannelMutingService.ts b/packages/backend/test/unit/ChannelMutingService.ts
new file mode 100644
index 000000000000..462f63f9c4f8
--- /dev/null
+++ b/packages/backend/test/unit/ChannelMutingService.ts
@@ -0,0 +1,336 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/* eslint-disable */
+
+import { afterEach, beforeEach, describe, expect } from '@jest/globals';
+import { Test, TestingModule } from '@nestjs/testing';
+import { GlobalModule } from '@/GlobalModule.js';
+import { CoreModule } from '@/core/CoreModule.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { IdService } from '@/core/IdService.js';
+import { ChannelMutingService } from '@/core/ChannelMutingService.js';
+import {
+	ChannelMutingRepository,
+	ChannelsRepository,
+	DriveFilesRepository,
+	MiChannel,
+	MiChannelMuting,
+	MiDriveFile,
+	MiUser,
+	UserProfilesRepository,
+	UsersRepository,
+} from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { setTimeout } from 'node:timers/promises';
+
+describe('ChannelMutingService', () => {
+	let app: TestingModule;
+	let service: ChannelMutingService;
+	let channelsRepository: ChannelsRepository;
+	let channelMutingRepository: ChannelMutingRepository;
+	let usersRepository: UsersRepository;
+	let userProfilesRepository: UserProfilesRepository;
+	let driveFilesRepository: DriveFilesRepository;
+	let idService: IdService;
+
+	let alice: MiUser;
+	let bob: MiUser;
+	let channel1: MiChannel;
+	let channel2: MiChannel;
+	let channel3: MiChannel;
+	let driveFile1: MiDriveFile;
+	let driveFile2: MiDriveFile;
+
+	async function createUser(data: Partial<MiUser> = {}) {
+		const user = await usersRepository
+			.insert({
+				id: idService.gen(),
+				username: 'username',
+				usernameLower: 'username',
+				...data,
+			})
+			.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+		await userProfilesRepository.insert({
+			userId: user.id,
+		});
+
+		return user;
+	}
+
+	async function createChannel(data: Partial<MiChannel> = {}) {
+		return await channelsRepository
+			.insert({
+				id: idService.gen(),
+				...data,
+			})
+			.then(x => channelsRepository.findOneByOrFail(x.identifiers[0]));
+	}
+
+	async function createChannelMuting(data: Partial<MiChannelMuting> = {}) {
+		return await channelMutingRepository
+			.insert({
+				id: idService.gen(),
+				...data,
+			})
+			.then(x => channelMutingRepository.findOneByOrFail(x.identifiers[0]));
+	}
+
+	async function fetchChannelMuting() {
+		return await channelMutingRepository.findBy({});
+	}
+
+	async function createDriveFile(data: Partial<MiDriveFile> = {}) {
+		return await driveFilesRepository
+			.insert({
+				id: idService.gen(),
+				md5: 'md5',
+				name: 'name',
+				size: 0,
+				type: 'type',
+				storedInternal: false,
+				url: 'url',
+				...data,
+			})
+			.then(x => driveFilesRepository.findOneByOrFail(x.identifiers[0]));
+	}
+
+	beforeAll(async () => {
+		app = await Test.createTestingModule({
+			imports: [
+				GlobalModule,
+				CoreModule,
+			],
+			providers: [
+				GlobalEventService,
+				IdService,
+				ChannelMutingService,
+			],
+		}).compile();
+
+		app.enableShutdownHooks();
+
+		service = app.get<ChannelMutingService>(ChannelMutingService);
+		idService = app.get<IdService>(IdService);
+		channelsRepository = app.get<ChannelsRepository>(DI.channelsRepository);
+		channelMutingRepository = app.get<ChannelMutingRepository>(DI.channelMutingRepository);
+		usersRepository = app.get<UsersRepository>(DI.usersRepository);
+		userProfilesRepository = app.get<UserProfilesRepository>(DI.userProfilesRepository);
+		driveFilesRepository = app.get<DriveFilesRepository>(DI.driveFilesRepository);
+	});
+
+	afterAll(async () => {
+		await app.close();
+	});
+
+	beforeEach(async () => {
+		alice = await createUser({ username: 'alice' });
+		bob = await createUser({ username: 'bob' });
+		driveFile1 = await createDriveFile();
+		driveFile2 = await createDriveFile();
+		channel1 = await createChannel({ name: 'channel1', userId: alice.id, bannerId: driveFile1.id });
+		channel2 = await createChannel({ name: 'channel2', userId: alice.id, bannerId: driveFile2.id });
+		channel3 = await createChannel({ name: 'channel3', userId: alice.id, bannerId: driveFile2.id });
+	});
+
+	afterEach(async () => {
+		await channelMutingRepository.delete({});
+		await channelsRepository.delete({});
+		await userProfilesRepository.delete({});
+		await usersRepository.delete({});
+	});
+
+	describe('list', () => {
+		test('default', async () => {
+			await createChannelMuting({ userId: alice.id, channelId: channel1.id });
+			await createChannelMuting({ userId: alice.id, channelId: channel2.id });
+			await createChannelMuting({ userId: bob.id, channelId: channel3.id });
+
+			const mutings = await service.list({ requestUserId: alice.id });
+
+			expect(mutings).toHaveLength(2);
+			expect(mutings[0].id).toBe(channel1.id);
+			expect(mutings[0].userId).toBe(alice.id);
+			expect(mutings[0].user).toBeFalsy();
+			expect(mutings[0].bannerId).toBe(driveFile1.id);
+			expect(mutings[0].banner).toBeFalsy();
+			expect(mutings[1].id).toBe(channel2.id);
+			expect(mutings[1].userId).toBe(alice.id);
+			expect(mutings[1].user).toBeFalsy();
+			expect(mutings[1].bannerId).toBe(driveFile2.id);
+			expect(mutings[1].banner).toBeFalsy();
+		});
+
+		test('withoutExpires', async () => {
+			const now = new Date();
+			const past = new Date(now);
+			const future = new Date(now);
+			past.setMinutes(past.getMinutes() - 1);
+			future.setMinutes(future.getMinutes() + 1);
+
+			await createChannelMuting({ userId: alice.id, channelId: channel1.id, expiresAt: past });
+			await createChannelMuting({ userId: alice.id, channelId: channel2.id, expiresAt: null });
+			await createChannelMuting({ userId: alice.id, channelId: channel3.id, expiresAt: future });
+
+			const mutings = await service.list({ requestUserId: alice.id });
+
+			expect(mutings).toHaveLength(2);
+			expect(mutings[0].id).toBe(channel2.id);
+			expect(mutings[1].id).toBe(channel3.id);
+		});
+
+		test('idOnly', async () => {
+			await createChannelMuting({ userId: alice.id, channelId: channel1.id });
+			await createChannelMuting({ userId: alice.id, channelId: channel2.id });
+			await createChannelMuting({ userId: bob.id, channelId: channel3.id });
+
+			const mutings = await service.list({ requestUserId: alice.id }, { idOnly: true });
+
+			expect(mutings).toHaveLength(2);
+			expect(mutings[0].id).toBe(channel1.id);
+			expect(mutings[1].id).toBe(channel2.id);
+		});
+
+		test('withoutExpires-idOnly', async () => {
+			const now = new Date();
+			const past = new Date(now);
+			const future = new Date(now);
+			past.setMinutes(past.getMinutes() - 1);
+			future.setMinutes(future.getMinutes() + 1);
+
+			await createChannelMuting({ userId: alice.id, channelId: channel1.id, expiresAt: past });
+			await createChannelMuting({ userId: alice.id, channelId: channel2.id, expiresAt: null });
+			await createChannelMuting({ userId: alice.id, channelId: channel3.id, expiresAt: future });
+
+			const mutings = await service.list({ requestUserId: alice.id }, { idOnly: true });
+
+			expect(mutings).toHaveLength(2);
+			expect(mutings[0].id).toBe(channel2.id);
+			expect(mutings[1].id).toBe(channel3.id);
+		});
+
+		test('joinUser', async () => {
+			await createChannelMuting({ userId: alice.id, channelId: channel1.id });
+			await createChannelMuting({ userId: alice.id, channelId: channel2.id });
+			await createChannelMuting({ userId: bob.id, channelId: channel3.id });
+
+			const mutings = await service.list({ requestUserId: alice.id }, { joinUser: true });
+
+			expect(mutings).toHaveLength(2);
+			expect(mutings[0].id).toBe(channel1.id);
+			expect(mutings[0].user).toEqual(alice);
+			expect(mutings[0].banner).toBeFalsy();
+			expect(mutings[1].id).toBe(channel2.id);
+			expect(mutings[1].user).toEqual(alice);
+			expect(mutings[1].banner).toBeFalsy();
+		});
+
+		test('joinBannerFile', async () => {
+			await createChannelMuting({ userId: alice.id, channelId: channel1.id });
+			await createChannelMuting({ userId: alice.id, channelId: channel2.id });
+			await createChannelMuting({ userId: bob.id, channelId: channel3.id });
+
+			const mutings = await service.list({ requestUserId: alice.id }, { joinBannerFile: true });
+
+			expect(mutings).toHaveLength(2);
+			expect(mutings[0].id).toBe(channel1.id);
+			expect(mutings[0].user).toBeFalsy();
+			expect(mutings[0].banner).toEqual(driveFile1);
+			expect(mutings[1].id).toBe(channel2.id);
+			expect(mutings[1].user).toBeFalsy();
+			expect(mutings[1].banner).toEqual(driveFile2);
+		});
+	});
+
+	describe('findExpiredMutings', () => {
+		test('default', async () => {
+			const now = new Date();
+			const future = new Date(now);
+			const past = new Date(now);
+			future.setMinutes(now.getMinutes() + 1);
+			past.setMinutes(now.getMinutes() - 1);
+
+			await createChannelMuting({ userId: alice.id, channelId: channel1.id, expiresAt: past });
+			await createChannelMuting({ userId: alice.id, channelId: channel2.id, expiresAt: future });
+			await createChannelMuting({ userId: bob.id, channelId: channel3.id, expiresAt: past });
+
+			const mutings = await service.findExpiredMutings();
+
+			expect(mutings).toHaveLength(2);
+			expect(mutings[0].channelId).toBe(channel1.id);
+			expect(mutings[1].channelId).toBe(channel3.id);
+		});
+	});
+
+	describe('isMuted', () => {
+		test('isMuted: true', async () => {
+			// キャッシュを読むのでServiceの機能を使って登録し、キャッシュを作成する
+			await service.mute({ requestUserId: alice.id, targetChannelId: channel1.id });
+			await service.mute({ requestUserId: alice.id, targetChannelId: channel2.id });
+
+			await setTimeout(500);
+
+			const result = await service.isMuted({ requestUserId: alice.id, targetChannelId: channel1.id });
+
+			expect(result).toBe(true);
+		});
+
+		test('isMuted: false', async () => {
+			await service.mute({ requestUserId: alice.id, targetChannelId: channel2.id });
+
+			await setTimeout(500);
+
+			const result = await service.isMuted({ requestUserId: alice.id, targetChannelId: channel1.id });
+
+			expect(result).toBe(false);
+		});
+	});
+
+	describe('mute', () => {
+		test('default', async () => {
+			await service.mute({ requestUserId: alice.id, targetChannelId: channel1.id });
+
+			const muting = await fetchChannelMuting();
+			expect(muting).toHaveLength(1);
+			expect(muting[0].channelId).toBe(channel1.id);
+		});
+	});
+
+	describe('unmute', () => {
+		test('default', async () => {
+			await createChannelMuting({ userId: alice.id, channelId: channel1.id });
+
+			let muting = await fetchChannelMuting();
+			expect(muting).toHaveLength(1);
+			expect(muting[0].channelId).toBe(channel1.id);
+
+			await service.unmute({ requestUserId: alice.id, targetChannelId: channel1.id });
+
+			muting = await fetchChannelMuting();
+			expect(muting).toHaveLength(0);
+		});
+	});
+
+	describe('eraseExpiredMutings', () => {
+		test('default', async () => {
+			const now = new Date();
+			const future = new Date(now);
+			const past = new Date(now);
+			future.setMinutes(now.getMinutes() + 1);
+			past.setMinutes(now.getMinutes() - 1);
+
+			await createChannelMuting({ userId: alice.id, channelId: channel1.id, expiresAt: past });
+			await createChannelMuting({ userId: alice.id, channelId: channel2.id, expiresAt: future });
+			await createChannelMuting({ userId: bob.id, channelId: channel3.id, expiresAt: past });
+
+			await service.eraseExpiredMutings();
+
+			const mutings = await fetchChannelMuting();
+			expect(mutings).toHaveLength(1);
+			expect(mutings[0].channelId).toBe(channel2.id);
+		});
+	});
+});
diff --git a/packages/backend/test/unit/NoteCreateService.ts b/packages/backend/test/unit/NoteCreateService.ts
index f2d4c8ffbb77..da351e24ab26 100644
--- a/packages/backend/test/unit/NoteCreateService.ts
+++ b/packages/backend/test/unit/NoteCreateService.ts
@@ -60,6 +60,7 @@ describe('NoteCreateService', () => {
 			replyUserHost: null,
 			renoteUserId: null,
 			renoteUserHost: null,
+			renoteChannelId: null,
 		};
 
 		const poll: IPoll = {
diff --git a/packages/backend/test/unit/misc/is-renote.ts b/packages/backend/test/unit/misc/is-renote.ts
index 0b713e8bf6b4..baa35fc495a8 100644
--- a/packages/backend/test/unit/misc/is-renote.ts
+++ b/packages/backend/test/unit/misc/is-renote.ts
@@ -43,6 +43,7 @@ const base: MiNote = {
 	replyUserHost: null,
 	renoteUserId: null,
 	renoteUserHost: null,
+	renoteChannelId: null,
 };
 
 describe('misc:is-renote', () => {
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 655e69461fe0..5736a7560021 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -123,22 +123,25 @@ const featuredPagination = computed(() => ({
 }));
 
 watch(() => props.channelId, async () => {
-	channel.value = await misskeyApi('channels/show', {
+	const _channel = await misskeyApi('channels/show', {
 		channelId: props.channelId,
 	});
-	favorited.value = channel.value.isFavorited ?? false;
-	if (favorited.value || channel.value.isFollowing) {
+
+	favorited.value = _channel.isFavorited ?? false;
+	if (favorited.value || _channel.isFollowing) {
 		tab.value = 'timeline';
 	}
 
-	if ((favorited.value || channel.value.isFollowing) && channel.value.lastNotedAt) {
-		const lastReadedAt: number = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.value.id}`) ?? 0;
-		const lastNotedAt = Date.parse(channel.value.lastNotedAt);
+	if ((favorited.value || _channel.isFollowing) && _channel.lastNotedAt) {
+		const lastReadedAt: number = miLocalStorage.getItemAsJson(`channelLastReadedAt:${_channel.id}`) ?? 0;
+		const lastNotedAt = Date.parse(_channel.lastNotedAt);
 
 		if (lastNotedAt > lastReadedAt) {
-			miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.value.id}`, lastNotedAt);
+			miLocalStorage.setItemAsJson(`channelLastReadedAt:${_channel.id}`, lastNotedAt);
 		}
 	}
+
+	channel.value = _channel;
 }, { immediate: true });
 
 function edit() {
@@ -178,6 +181,53 @@ async function unfavorite() {
 	});
 }
 
+async function mute() {
+	if (!channel.value) return;
+	const _channel = channel.value;
+
+	const { canceled, result: period } = await os.select({
+		title: i18n.ts.mutePeriod,
+		items: [{
+			value: 'indefinitely', text: i18n.ts.indefinitely,
+		}, {
+			value: 'tenMinutes', text: i18n.ts.tenMinutes,
+		}, {
+			value: 'oneHour', text: i18n.ts.oneHour,
+		}, {
+			value: 'oneDay', text: i18n.ts.oneDay,
+		}, {
+			value: 'oneWeek', text: i18n.ts.oneWeek,
+		}],
+		default: 'indefinitely',
+	});
+	if (canceled) return;
+
+	const expiresAt = period === 'indefinitely' ? null
+		: period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10)
+		: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
+		: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
+		: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
+		: null;
+
+	os.apiWithDialog('channels/mute/create', {
+		channelId: _channel.id,
+		expiresAt,
+	}).then(() => {
+		_channel.isMuting = true;
+	});
+}
+
+async function unmute() {
+	if (!channel.value) return;
+	const _channel = channel.value;
+
+	os.apiWithDialog('channels/mute/delete', {
+		channelId: _channel.id,
+	}).then(() => {
+		_channel.isMuting = false;
+	});
+}
+
 async function search() {
 	if (!channel.value) return;
 
@@ -233,6 +283,24 @@ const headerActions = computed(() => {
 			});
 		}
 
+		if (!channel.value.isMuting) {
+			headerItems.push({
+				icon: 'ti ti-volume',
+				text: i18n.ts.mute,
+				handler: async (): Promise<void> => {
+					await mute();
+				},
+			});
+		} else {
+			headerItems.push({
+				icon: 'ti ti-volume-off',
+				text: i18n.ts.unmute,
+				handler: async (): Promise<void> => {
+					await unmute();
+				},
+			});
+		}
+
 		if (($i && $i.id === channel.value.userId) || iAmModerator) {
 			headerItems.push({
 				icon: 'ti ti-settings',
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 7098b522056a..3567834ea485 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -834,6 +834,15 @@ type ChannelsFollowedResponse = operations['channels___followed']['responses']['
 // @public (undocumented)
 type ChannelsFollowRequest = operations['channels___follow']['requestBody']['content']['application/json'];
 
+// @public (undocumented)
+type ChannelsMuteCreateRequest = operations['channels___mute___create']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type ChannelsMuteDeleteRequest = operations['channels___mute___delete']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
+type ChannelsMuteListResponse = operations['channels___mute___list']['responses']['200']['content']['application/json'];
+
 // @public (undocumented)
 type ChannelsMyFavoritesResponse = operations['channels___my-favorites']['responses']['200']['content']['application/json'];
 
@@ -1415,6 +1424,9 @@ declare namespace entities {
         ChannelsMyFavoritesResponse,
         ChannelsSearchRequest,
         ChannelsSearchResponse,
+        ChannelsMuteCreateRequest,
+        ChannelsMuteDeleteRequest,
+        ChannelsMuteListResponse,
         ChartsActiveUsersRequest,
         ChartsActiveUsersResponse,
         ChartsApRequestRequest,
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index edaa0498e9ff..73dc4e5e03c4 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -1380,6 +1380,39 @@ declare module '../api.js' {
       credential?: string | null,
     ): Promise<SwitchCaseResponseType<E, P>>;
 
+    /**
+     * No description provided.
+     * 
+     * **Credential required**: *Yes* / **Permission**: *write:channels*
+     */
+    request<E extends 'channels/mute/create', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
+    /**
+     * No description provided.
+     * 
+     * **Credential required**: *Yes* / **Permission**: *write:channels*
+     */
+    request<E extends 'channels/mute/delete', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
+    /**
+     * No description provided.
+     * 
+     * **Credential required**: *Yes* / **Permission**: *read:channels*
+     */
+    request<E extends 'channels/mute/list', P extends Endpoints[E]['req']>(
+      endpoint: E,
+      params: P,
+      credential?: string | null,
+    ): Promise<SwitchCaseResponseType<E, P>>;
+
     /**
      * No description provided.
      * 
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 982717597b35..252a1cc37d99 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -180,6 +180,9 @@ import type {
 	ChannelsMyFavoritesResponse,
 	ChannelsSearchRequest,
 	ChannelsSearchResponse,
+	ChannelsMuteCreateRequest,
+	ChannelsMuteDeleteRequest,
+	ChannelsMuteListResponse,
 	ChartsActiveUsersRequest,
 	ChartsActiveUsersResponse,
 	ChartsApRequestRequest,
@@ -710,6 +713,9 @@ export type Endpoints = {
 	'channels/unfavorite': { req: ChannelsUnfavoriteRequest; res: EmptyResponse };
 	'channels/my-favorites': { req: EmptyRequest; res: ChannelsMyFavoritesResponse };
 	'channels/search': { req: ChannelsSearchRequest; res: ChannelsSearchResponse };
+	'channels/mute/create': { req: ChannelsMuteCreateRequest; res: EmptyResponse };
+	'channels/mute/delete': { req: ChannelsMuteDeleteRequest; res: EmptyResponse };
+	'channels/mute/list': { req: EmptyRequest; res: ChannelsMuteListResponse };
 	'charts/active-users': { req: ChartsActiveUsersRequest; res: ChartsActiveUsersResponse };
 	'charts/ap-request': { req: ChartsApRequestRequest; res: ChartsApRequestResponse };
 	'charts/drive': { req: ChartsDriveRequest; res: ChartsDriveResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index e4299d62c702..f52228bb07fa 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -183,6 +183,9 @@ export type ChannelsUnfavoriteRequest = operations['channels___unfavorite']['req
 export type ChannelsMyFavoritesResponse = operations['channels___my-favorites']['responses']['200']['content']['application/json'];
 export type ChannelsSearchRequest = operations['channels___search']['requestBody']['content']['application/json'];
 export type ChannelsSearchResponse = operations['channels___search']['responses']['200']['content']['application/json'];
+export type ChannelsMuteCreateRequest = operations['channels___mute___create']['requestBody']['content']['application/json'];
+export type ChannelsMuteDeleteRequest = operations['channels___mute___delete']['requestBody']['content']['application/json'];
+export type ChannelsMuteListResponse = operations['channels___mute___list']['responses']['200']['content']['application/json'];
 export type ChartsActiveUsersRequest = operations['charts___active-users']['requestBody']['content']['application/json'];
 export type ChartsActiveUsersResponse = operations['charts___active-users']['responses']['200']['content']['application/json'];
 export type ChartsApRequestRequest = operations['charts___ap-request']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 75a99263d027..3e745171a874 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -1141,6 +1141,33 @@ export type paths = {
      */
     post: operations['channels___search'];
   };
+  '/channels/mute/create': {
+    /**
+     * channels/mute/create
+     * @description No description provided.
+     *
+     * **Credential required**: *Yes* / **Permission**: *write:channels*
+     */
+    post: operations['channels___mute___create'];
+  };
+  '/channels/mute/delete': {
+    /**
+     * channels/mute/delete
+     * @description No description provided.
+     *
+     * **Credential required**: *Yes* / **Permission**: *write:channels*
+     */
+    post: operations['channels___mute___delete'];
+  };
+  '/channels/mute/list': {
+    /**
+     * channels/mute/list
+     * @description No description provided.
+     *
+     * **Credential required**: *Yes* / **Permission**: *read:channels*
+     */
+    post: operations['channels___mute___list'];
+  };
   '/charts/active-users': {
     /**
      * charts/active-users
@@ -4618,6 +4645,7 @@ export type components = {
       allowRenoteToExternal: boolean;
       isFollowing?: boolean;
       isFavorited?: boolean;
+      isMuting?: boolean;
       pinnedNotes?: components['schemas']['Note'][];
     };
     QueueCount: {
@@ -12696,6 +12724,158 @@ export type operations = {
       };
     };
   };
+  /**
+   * channels/mute/create
+   * @description No description provided.
+   *
+   * **Credential required**: *Yes* / **Permission**: *write:channels*
+   */
+  channels___mute___create: {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** Format: misskey:id */
+          channelId: string;
+          /** @description A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute. */
+          expiresAt?: number | null;
+        };
+      };
+    };
+    responses: {
+      /** @description OK (without any results) */
+      204: {
+        content: never;
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * channels/mute/delete
+   * @description No description provided.
+   *
+   * **Credential required**: *Yes* / **Permission**: *write:channels*
+   */
+  channels___mute___delete: {
+    requestBody: {
+      content: {
+        'application/json': {
+          /** Format: misskey:id */
+          channelId: string;
+        };
+      };
+    };
+    responses: {
+      /** @description OK (without any results) */
+      204: {
+        content: never;
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
+  /**
+   * channels/mute/list
+   * @description No description provided.
+   *
+   * **Credential required**: *Yes* / **Permission**: *read:channels*
+   */
+  channels___mute___list: {
+    responses: {
+      /** @description OK (with results) */
+      200: {
+        content: {
+          'application/json': components['schemas']['Channel'][];
+        };
+      };
+      /** @description Client error */
+      400: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Authentication error */
+      401: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Forbidden error */
+      403: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description I'm Ai */
+      418: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+      /** @description Internal server error */
+      500: {
+        content: {
+          'application/json': components['schemas']['Error'];
+        };
+      };
+    };
+  };
   /**
    * charts/active-users
    * @description No description provided.