diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf62565b8c22cac9fa09f57836508a68e02d08a8..ae652c310a3d9f430259ca00dd2998202a4198e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
 ## 12.x.x (unreleased)
 
 ### Improvements
+- スレッドミュート機能
 
 ### Bugfixes
 - リレー向けのActivityが一部実装で除外されてしまうことがあるのを修正
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index dbb0bf166498f945cf87bd64827231d3622fde61..1326369f83765d85a4e015f2f45de195136eb58a 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -800,6 +800,8 @@ manageAccounts: "アカウントを管理"
 makeReactionsPublic: "リアクション一覧を公開する"
 makeReactionsPublicDescription: "あなたがしたリアクション一覧を誰でも見れるようにします。"
 classic: "クラシック"
+muteThread: "スレッドをミュート"
+unmuteThread: "スレッドのミュートを解除"
 
 _signup:
   almostThere: "ほとんど完了です"
diff --git a/migration/1635500777168-note-thread-mute.ts b/migration/1635500777168-note-thread-mute.ts
new file mode 100644
index 0000000000000000000000000000000000000000..aed10d18d79bd2b159bc8a7547455cde27069912
--- /dev/null
+++ b/migration/1635500777168-note-thread-mute.ts
@@ -0,0 +1,26 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class noteThreadMute1635500777168 implements MigrationInterface {
+    name = 'noteThreadMute1635500777168'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`CREATE TABLE "note_thread_muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "threadId" character varying(256) NOT NULL, CONSTRAINT "PK_ec5936d94d1a0369646d12a3a47" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_29c11c7deb06615076f8c95b80" ON "note_thread_muting" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_c426394644267453e76f036926" ON "note_thread_muting" ("threadId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ae7aab18a2641d3e5f25e0c4ea" ON "note_thread_muting" ("userId", "threadId") `);
+        await queryRunner.query(`ALTER TABLE "note" ADD "threadId" character varying(256)`);
+        await queryRunner.query(`CREATE INDEX "IDX_d4ebdef929896d6dc4a3c5bb48" ON "note" ("threadId") `);
+        await queryRunner.query(`ALTER TABLE "note_thread_muting" ADD CONSTRAINT "FK_29c11c7deb06615076f8c95b80a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "note_thread_muting" DROP CONSTRAINT "FK_29c11c7deb06615076f8c95b80a"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_d4ebdef929896d6dc4a3c5bb48"`);
+        await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "threadId"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_ae7aab18a2641d3e5f25e0c4ea"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_c426394644267453e76f036926"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_29c11c7deb06615076f8c95b80"`);
+        await queryRunner.query(`DROP TABLE "note_thread_muting"`);
+    }
+
+}
diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue
index 40b0a68c583391402643922bc68f36a2c5a59b2c..568a2360d167ae230c5ef288a9a686aab20502f6 100644
--- a/src/client/components/note-detailed.vue
+++ b/src/client/components/note-detailed.vue
@@ -601,6 +601,12 @@ export default defineComponent({
 			});
 		},
 
+		toggleThreadMute(mute: boolean) {
+			os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
+				noteId: this.appearNote.id
+			});
+		},
+
 		getMenu() {
 			let menu;
 			if (this.$i) {
@@ -657,6 +663,15 @@ export default defineComponent({
 					text: this.$ts.watch,
 					action: () => this.toggleWatch(true)
 				}) : undefined,
+				statePromise.then(state => state.isMutedThread ? {
+					icon: 'fas fa-comment-slash',
+					text: this.$ts.unmuteThread,
+					action: () => this.toggleThreadMute(false)
+				} : {
+					icon: 'fas fa-comment-slash',
+					text: this.$ts.muteThread,
+					action: () => this.toggleThreadMute(true)
+				}),
 				this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
 					icon: 'fas fa-thumbtack',
 					text: this.$ts.unpin,
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 91a3e3b87d6ead08f3407fdc46cfc18205a14189..681e819a225cf62081167fa5cf0763ac222b3506 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -576,6 +576,12 @@ export default defineComponent({
 			});
 		},
 
+		toggleThreadMute(mute: boolean) {
+			os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
+				noteId: this.appearNote.id
+			});
+		},
+
 		getMenu() {
 			let menu;
 			if (this.$i) {
@@ -632,6 +638,15 @@ export default defineComponent({
 					text: this.$ts.watch,
 					action: () => this.toggleWatch(true)
 				}) : undefined,
+				statePromise.then(state => state.isMutedThread ? {
+					icon: 'fas fa-comment-slash',
+					text: this.$ts.unmuteThread,
+					action: () => this.toggleThreadMute(false)
+				} : {
+					icon: 'fas fa-comment-slash',
+					text: this.$ts.muteThread,
+					action: () => this.toggleThreadMute(true)
+				}),
 				this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
 					icon: 'fas fa-thumbtack',
 					text: this.$ts.unpin,
diff --git a/src/db/postgre.ts b/src/db/postgre.ts
index 4f4047b61360f0e7538ad0f27d57a4863b3e3669..f52c2ab72232f0d9783e8ab20ca507e1405c5ac4 100644
--- a/src/db/postgre.ts
+++ b/src/db/postgre.ts
@@ -17,6 +17,7 @@ import { PollVote } from '@/models/entities/poll-vote';
 import { Note } from '@/models/entities/note';
 import { NoteReaction } from '@/models/entities/note-reaction';
 import { NoteWatching } from '@/models/entities/note-watching';
+import { NoteThreadMuting } from '@/models/entities/note-thread-muting';
 import { NoteUnread } from '@/models/entities/note-unread';
 import { Notification } from '@/models/entities/notification';
 import { Meta } from '@/models/entities/meta';
@@ -138,6 +139,7 @@ export const entities = [
 	NoteFavorite,
 	NoteReaction,
 	NoteWatching,
+	NoteThreadMuting,
 	NoteUnread,
 	Page,
 	PageLike,
diff --git a/src/models/entities/note-thread-muting.ts b/src/models/entities/note-thread-muting.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b438522a4ce14456bdca20f6cbd3ab898f30b1ea
--- /dev/null
+++ b/src/models/entities/note-thread-muting.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'threadId'], { unique: true })
+export class NoteThreadMuting {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 256,
+	})
+	public threadId: string;
+}
diff --git a/src/models/entities/note.ts b/src/models/entities/note.ts
index 9a85532637bde8788834424190945ac8c16467fe..4a5411f93dfcce519bb0234ad83492f6c8a0baa2 100644
--- a/src/models/entities/note.ts
+++ b/src/models/entities/note.ts
@@ -47,6 +47,12 @@ export class Note {
 	@JoinColumn()
 	public renote: Note | null;
 
+	@Index()
+	@Column('varchar', {
+		length: 256, nullable: true
+	})
+	public threadId: string | null;
+
 	@Column('varchar', {
 		length: 8192, nullable: true
 	})
diff --git a/src/models/index.ts b/src/models/index.ts
index 4c6f19eaff5aadf6a94a646360c1a8eef032f378..7154cca550299030df3ca6fb11b8053d21cb6ce0 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -7,6 +7,7 @@ import { PollVote } from './entities/poll-vote';
 import { Meta } from './entities/meta';
 import { SwSubscription } from './entities/sw-subscription';
 import { NoteWatching } from './entities/note-watching';
+import { NoteThreadMuting } from './entities/note-thread-muting';
 import { NoteUnread } from './entities/note-unread';
 import { RegistrationTicket } from './entities/registration-tickets';
 import { UserRepository } from './repositories/user';
@@ -69,6 +70,7 @@ export const Apps = getCustomRepository(AppRepository);
 export const Notes = getCustomRepository(NoteRepository);
 export const NoteFavorites = getCustomRepository(NoteFavoriteRepository);
 export const NoteWatchings = getRepository(NoteWatching);
+export const NoteThreadMutings = getRepository(NoteThreadMuting);
 export const NoteReactions = getCustomRepository(NoteReactionRepository);
 export const NoteUnreads = getRepository(NoteUnread);
 export const Polls = getRepository(Poll);
diff --git a/src/server/api/common/generate-muted-note-thread-query.ts b/src/server/api/common/generate-muted-note-thread-query.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7e2cbd498b8602946be478cf6d4a518dbf5b15ef
--- /dev/null
+++ b/src/server/api/common/generate-muted-note-thread-query.ts
@@ -0,0 +1,17 @@
+import { User } from '@/models/entities/user';
+import { NoteThreadMutings } from '@/models/index';
+import { Brackets, SelectQueryBuilder } from 'typeorm';
+
+export function generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
+	const mutedQuery = NoteThreadMutings.createQueryBuilder('threadMuted')
+		.select('threadMuted.threadId')
+		.where('threadMuted.userId = :userId', { userId: me.id });
+
+	q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
+	q.andWhere(new Brackets(qb => { qb
+		.where(`note.threadId IS NULL`)
+		.orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`);
+	}));
+
+	q.setParameters(mutedQuery.getParameters());
+}
diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts
index 74f7911bfe215c09bf9a00940606d852ce8e6e9e..ffaebd6c95fd18550b44082ceb99e8a9dec7f474 100644
--- a/src/server/api/endpoints/notes/mentions.ts
+++ b/src/server/api/endpoints/notes/mentions.ts
@@ -8,6 +8,7 @@ import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 import { Brackets } from 'typeorm';
 import { generateBlockedUserQuery } from '../../common/generate-block-query';
+import { generateMutedNoteThreadQuery } from '../../common/generate-muted-note-thread-query';
 
 export const meta = {
 	tags: ['notes'],
@@ -67,6 +68,7 @@ export default define(meta, async (ps, user) => {
 
 	generateVisibilityQuery(query, user);
 	generateMutedUserQuery(query, user);
+	generateMutedNoteThreadQuery(query, user);
 	generateBlockedUserQuery(query, user);
 
 	if (ps.visibility) {
diff --git a/src/server/api/endpoints/notes/state.ts b/src/server/api/endpoints/notes/state.ts
index 489902435d66f10e0b873c7148160da6d414d13f..b3913a5e79e40668c89c9a530bca12ab69a2efa5 100644
--- a/src/server/api/endpoints/notes/state.ts
+++ b/src/server/api/endpoints/notes/state.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import { ID } from '@/misc/cafy-id';
 import define from '../../define';
-import { NoteFavorites, NoteWatchings } from '@/models/index';
+import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models/index';
 
 export const meta = {
 	tags: ['notes'],
@@ -25,31 +25,45 @@ export const meta = {
 			isWatching: {
 				type: 'boolean' as const,
 				optional: false as const, nullable: false as const
-			}
+			},
+			isMutedThread: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
 		}
 	}
 };
 
 export default define(meta, async (ps, user) => {
-	const [favorite, watching] = await Promise.all([
+	const note = await Notes.findOneOrFail(ps.noteId);
+
+	const [favorite, watching, threadMuting] = await Promise.all([
 		NoteFavorites.count({
 			where: {
 				userId: user.id,
-				noteId: ps.noteId
+				noteId: note.id,
 			},
 			take: 1
 		}),
 		NoteWatchings.count({
 			where: {
 				userId: user.id,
-				noteId: ps.noteId
+				noteId: note.id,
 			},
 			take: 1
-		})
+		}),
+		NoteThreadMutings.count({
+			where: {
+				userId: user.id,
+				threadId: note.threadId || note.id,
+			},
+			take: 1
+		}),
 	]);
 
 	return {
 		isFavorited: favorite !== 0,
-		isWatching: watching !== 0
+		isWatching: watching !== 0,
+		isMutedThread: threadMuting !== 0,
 	};
 });
diff --git a/src/server/api/endpoints/notes/thread-muting/create.ts b/src/server/api/endpoints/notes/thread-muting/create.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2010d54331ed7e36d6eec80b481cfecdfa299acc
--- /dev/null
+++ b/src/server/api/endpoints/notes/thread-muting/create.ts
@@ -0,0 +1,54 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { getNote } from '../../../common/getters';
+import { ApiError } from '../../../error';
+import { Notes, NoteThreadMutings } from '@/models';
+import { genId } from '@/misc/gen-id';
+import readNote from '@/services/note/read';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: '5ff67ada-ed3b-2e71-8e87-a1a421e177d2'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	const mutedNotes = await Notes.find({
+		where: [{
+			id: note.threadId || note.id,
+		}, {
+			threadId: note.threadId || note.id,
+		}],
+	});
+
+	await readNote(user.id, mutedNotes);
+
+	await NoteThreadMutings.insert({
+		id: genId(),
+		createdAt: new Date(),
+		threadId: note.threadId || note.id,
+		userId: user.id,
+	});
+});
diff --git a/src/server/api/endpoints/notes/thread-muting/delete.ts b/src/server/api/endpoints/notes/thread-muting/delete.ts
new file mode 100644
index 0000000000000000000000000000000000000000..05d5691870c264e9afcb37018f3c982e7bdf140d
--- /dev/null
+++ b/src/server/api/endpoints/notes/thread-muting/delete.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { getNote } from '../../../common/getters';
+import { ApiError } from '../../../error';
+import { NoteThreadMutings } from '@/models';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: 'bddd57ac-ceb3-b29d-4334-86ea5fae481a'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	await NoteThreadMutings.delete({
+		threadId: note.threadId || note.id,
+		userId: user.id,
+	});
+});
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 8c996bdba661dd26512805076a13ebed3594d619..69d854ab1a45e033a8524fe0392758c1feea88d9 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -10,13 +10,13 @@ import { resolveUser } from '@/remote/resolve-user';
 import config from '@/config/index';
 import { updateHashtags } from '../update-hashtag';
 import { concat } from '@/prelude/array';
-import insertNoteUnread from './unread';
+import { insertNoteUnread } from '@/services/note/unread';
 import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
 import { extractMentions } from '@/misc/extract-mentions';
 import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm';
 import { extractHashtags } from '@/misc/extract-hashtags';
 import { Note, IMentionedRemoteUsers } from '@/models/entities/note';
-import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes, Channels, ChannelFollowings, Blockings } from '@/models/index';
+import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes, Channels, ChannelFollowings, Blockings, NoteThreadMutings } from '@/models/index';
 import { DriveFile } from '@/models/entities/drive-file';
 import { App } from '@/models/entities/app';
 import { Not, getConnection, In } from 'typeorm';
@@ -344,8 +344,15 @@ export default async (user: { id: User['id']; username: User['username']; host:
 
 			// 通知
 			if (data.reply.userHost === null) {
-				nm.push(data.reply.userId, 'reply');
-				publishMainStream(data.reply.userId, 'reply', noteObj);
+				const threadMuted = await NoteThreadMutings.findOne({
+					userId: data.reply.userId,
+					threadId: data.reply.threadId || data.reply.id,
+				});
+
+				if (!threadMuted) {
+					nm.push(data.reply.userId, 'reply');
+					publishMainStream(data.reply.userId, 'reply', noteObj);
+				}
 			}
 		}
 
@@ -459,6 +466,11 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O
 		replyId: data.reply ? data.reply.id : null,
 		renoteId: data.renote ? data.renote.id : null,
 		channelId: data.channel ? data.channel.id : null,
+		threadId: data.reply
+			? data.reply.threadId
+				? data.reply.threadId
+				: data.reply.id
+			: null,
 		name: data.name,
 		text: data.text,
 		hasPoll: data.poll != null,
@@ -581,6 +593,15 @@ async function notifyToWatchersOfReplyee(reply: Note, user: { id: User['id']; },
 
 async function createMentionedEvents(mentionedUsers: User[], note: Note, nm: NotificationManager) {
 	for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) {
+		const threadMuted = await NoteThreadMutings.findOne({
+			userId: u.id,
+			threadId: note.threadId || note.id,
+		});
+
+		if (threadMuted) {
+			continue;
+		}
+
 		const detailPackedNote = await Notes.pack(note, u, {
 			detail: true
 		});
diff --git a/src/services/note/unread.ts b/src/services/note/unread.ts
index 4a9df6083ccc2aedbe96516d32e191a9d348ef26..29d2b54af84f74303ca97860e94de356504aa22f 100644
--- a/src/services/note/unread.ts
+++ b/src/services/note/unread.ts
@@ -1,10 +1,10 @@
 import { Note } from '@/models/entities/note';
 import { publishMainStream } from '@/services/stream';
 import { User } from '@/models/entities/user';
-import { Mutings, NoteUnreads } from '@/models/index';
+import { Mutings, NoteThreadMutings, NoteUnreads } from '@/models/index';
 import { genId } from '@/misc/gen-id';
 
-export default async function(userId: User['id'], note: Note, params: {
+export async function insertNoteUnread(userId: User['id'], note: Note, params: {
 	// NOTE: isSpecifiedがtrueならisMentionedは必ずfalse
 	isSpecified: boolean;
 	isMentioned: boolean;
@@ -17,6 +17,13 @@ export default async function(userId: User['id'], note: Note, params: {
 	if (mute.map(m => m.muteeId).includes(note.userId)) return;
 	//#endregion
 
+	// スレッドミュート
+	const threadMute = await NoteThreadMutings.findOne({
+		userId: userId,
+		threadId: note.threadId || note.id,
+	});
+	if (threadMute) return;
+
 	const unread = {
 		id: genId(),
 		noteId: note.id,
diff --git a/test/thread-mute.ts b/test/thread-mute.ts
new file mode 100644
index 0000000000000000000000000000000000000000..95601cd903515a451ed7778bcb542f7adb3ee510
--- /dev/null
+++ b/test/thread-mute.ts
@@ -0,0 +1,103 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import * as childProcess from 'child_process';
+import { async, signup, request, post, react, connectStream, startServer, shutdownServer } from './utils';
+
+describe('Note thread mute', () => {
+	let p: childProcess.ChildProcess;
+
+	let alice: any;
+	let bob: any;
+	let carol: any;
+
+	before(async () => {
+		p = await startServer();
+		alice = await signup({ username: 'alice' });
+		bob = await signup({ username: 'bob' });
+		carol = await signup({ username: 'carol' });
+	});
+
+	after(async () => {
+		await shutdownServer(p);
+	});
+
+	it('notes/mentions にミュートしているスレッドの投稿が含まれない', async(async () => {
+		const bobNote = await post(bob, { text: '@alice @carol root note' });
+		const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
+
+		await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
+
+		const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
+		const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
+
+		const res = await request('/notes/mentions', {}, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(Array.isArray(res.body), true);
+		assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+		assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false);
+		assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false);
+	}));
+
+	it('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async(async () => {
+		// 状態リセット
+		await request('/i/read-all-unread-notes', {}, alice);
+
+		const bobNote = await post(bob, { text: '@alice @carol root note' });
+
+		await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
+
+		const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
+
+		const res = await request('/i', {}, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(res.body.hasUnreadMentions, false);
+	}));
+
+	it('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => {
+		// 状態リセット
+		await request('/i/read-all-unread-notes', {}, alice);
+
+		const bobNote = await post(bob, { text: '@alice @carol root note' });
+
+		await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
+
+		let fired = false;
+
+		const ws = await connectStream(alice, 'main', async ({ type, body }) => {
+			if (type === 'unreadMention') {
+				if (body === bobNote.id) return;
+				fired = true;
+			}
+		});
+
+		const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
+
+		setTimeout(() => {
+			assert.strictEqual(fired, false);
+			ws.close();
+			done();
+		}, 5000);
+	}));
+
+	it('i/notifications にミュートしているスレッドの通知が含まれない', async(async () => {
+		const bobNote = await post(bob, { text: '@alice @carol root note' });
+		const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
+
+		await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
+
+		const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
+		const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
+
+		const res = await request('/i/notifications', {}, alice);
+
+		assert.strictEqual(res.status, 200);
+		assert.strictEqual(Array.isArray(res.body), true);
+		assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false);
+		assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false);
+
+		// NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
+	}));
+});
diff --git a/test/utils.ts b/test/utils.ts
index 1a0c54463dd88cf2033e6dd75d5b06a1f19c0cf9..54bcf65ab15e3da9c66da3047e61a67d7966087c 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -1,5 +1,6 @@
 import * as fs from 'fs';
 import * as WebSocket from 'ws';
+import * as misskey from 'misskey-js';
 import fetch from 'node-fetch';
 const FormData = require('form-data');
 import * as childProcess from 'child_process';
@@ -52,7 +53,7 @@ export const signup = async (params?: any): Promise<any> => {
 	return res.body;
 };
 
-export const post = async (user: any, params?: any): Promise<any> => {
+export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
 	const q = Object.assign({
 		text: 'test'
 	}, params);