diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 69fde53f54..bb3a151add 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -832,7 +832,7 @@ desktop/views/components/settings.vue:
   profile: "プロフィール"
   notification: "通知"
   apps: "アプリ"
-  mute: "ミュート"
+  mute-and-block: "ミュート/ブロック"
   blocking: "ブロック"
   security: "セキュリティ"
   signin: "サインイン履歴"
@@ -974,11 +974,12 @@ common/views/components/drive-settings.vue:
   in-use: "使用中"
   stats: "統計"
 
-desktop/views/components/settings.mute.vue:
-  no-users: "ミュートしているユーザーはいません"
-
-desktop/views/components/settings.blocking.vue:
-  no-users: "ブロックしているユーザーはいません"
+common/views/components/mute-and-block.vue:
+  mute-and-block: "ミュートとブロック"
+  mute: "ミュート"
+  block: "ブロック"
+  no-muted-users: "ミュートしているユーザーはいません"
+  no-blocked-users: "ブロックしているユーザーはいません"
 
 desktop/views/components/settings.password.vue:
   reset: "パスワードを変更する"
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 33216e459d..2f2217f8d2 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -1,5 +1,6 @@
 import Vue from 'vue';
 
+import muteAndBlock from './mute-and-block.vue';
 import error from './error.vue';
 import apiSettings from './api-settings.vue';
 import driveSettings from './drive-settings.vue';
@@ -50,6 +51,7 @@ import uiInfo from './ui/info.vue';
 import formButton from './ui/form/button.vue';
 import formRadio from './ui/form/radio.vue';
 
+Vue.component('mk-mute-and-block', muteAndBlock);
 Vue.component('mk-error', error);
 Vue.component('mk-api-settings', apiSettings);
 Vue.component('mk-drive-settings', driveSettings);
diff --git a/src/client/app/common/views/components/mute-and-block.vue b/src/client/app/common/views/components/mute-and-block.vue
new file mode 100644
index 0000000000..b244f975bb
--- /dev/null
+++ b/src/client/app/common/views/components/mute-and-block.vue
@@ -0,0 +1,56 @@
+<template>
+<ui-card>
+	<div slot="title">%fa:ban% %i18n:@mute-and-block%</div>
+
+	<section>
+		<header>%i18n:@mute%</header>
+		<ui-info v-if="!muteFetching && mute.length == 0">
+			<p>%i18n:@no-muted-users%</p>
+		</ui-info>
+		<div class="users" v-if="mute.length != 0">
+			<div v-for="user in mute" :key="user.id">
+				<p><b>{{ user | userName }}</b> @{{ user | acct }}</p>
+			</div>
+		</div>
+	</section>
+
+	<section>
+		<header>%i18n:@block%</header>
+		<ui-info v-if="!blockFetching && block.length == 0">
+			<p>%i18n:@no-blocked-users%</p>
+		</ui-info>
+		<div class="users" v-if="block.length != 0">
+			<div v-for="user in block" :key="user.id">
+				<p><b>{{ user | userName }}</b> @{{ user | acct }}</p>
+			</div>
+		</div>
+	</section>
+</ui-card>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	data() {
+		return {
+			muteFetching: true,
+			blockFetching: true,
+			mute: [],
+			block: []
+		};
+	},
+
+	mounted() {
+		(this as any).api('mute/list').then(mute => {
+			this.mute = mute.map(x => x.mutee);
+			this.muteFetching = false;
+		});
+
+		(this as any).api('blocking/list').then(blocking => {
+			this.block = blocking.map(x => x.blockee);
+			this.blockFetching = false;
+		});
+	}
+});
+</script>
diff --git a/src/client/app/desktop/views/components/settings.blocking.vue b/src/client/app/desktop/views/components/settings.blocking.vue
deleted file mode 100644
index 43e4bf9209..0000000000
--- a/src/client/app/desktop/views/components/settings.blocking.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-<div>
-	<div class="none ui info" v-if="!fetching && users.length == 0">
-		<p>%fa:info-circle%%i18n:@no-users%</p>
-	</div>
-	<div class="users" v-if="users.length != 0">
-		<div v-for="user in users" :key="user.id">
-			<p><b>{{ user | userName }}</b> @{{ user | acct }}</p>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	data() {
-		return {
-			fetching: true,
-			users: []
-		};
-	},
-	mounted() {
-		(this as any).api('blocking/list').then(x => {
-			this.users = x.users;
-			this.fetching = false;
-		});
-	}
-});
-</script>
diff --git a/src/client/app/desktop/views/components/settings.mute.vue b/src/client/app/desktop/views/components/settings.mute.vue
deleted file mode 100644
index 792afa923f..0000000000
--- a/src/client/app/desktop/views/components/settings.mute.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-<div>
-	<div class="none ui info" v-if="!fetching && users.length == 0">
-		<p>%fa:info-circle%%i18n:@no-users%</p>
-	</div>
-	<div class="users" v-if="users.length != 0">
-		<div v-for="user in users" :key="user.id">
-			<p><b>{{ user | userName }}</b> @{{ user | acct }}</p>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	data() {
-		return {
-			fetching: true,
-			users: []
-		};
-	},
-	mounted() {
-		(this as any).api('mute/list').then(x => {
-			this.users = x.users;
-			this.fetching = false;
-		});
-	}
-});
-</script>
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index e77320bef3..74cedf9ecc 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -7,8 +7,7 @@
 		<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p>
 		<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:common.drive%</p>
 		<p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'">%fa:hashtag .fw%%i18n:@tags%</p>
-		<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p>
-		<p :class="{ active: page == 'blocking' }" @mousedown="page = 'blocking'">%fa:ban .fw%%i18n:@blocking%</p>
+		<p :class="{ active: page == 'muteAndBlock' }" @mousedown="page = 'muteAndBlock'">%fa:ban .fw%%i18n:@mute-and-block%</p>
 		<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p>
 		<p :class="{ active: page == 'security' }" @mousedown="page = 'security'">%fa:unlock-alt .fw%%i18n:@security%</p>
 		<p :class="{ active: page == 'api' }" @mousedown="page = 'api'">%fa:key .fw%API</p>
@@ -201,19 +200,9 @@
 			</section>
 		</ui-card>
 
-		<ui-card class="mute" v-show="page == 'mute'">
-			<div slot="title">%fa:ban% %i18n:@mute%</div>
-			<section>
-				<x-mute/>
-			</section>
-		</ui-card>
-
-		<ui-card class="blocking" v-show="page == 'blocking'">
-			<div slot="title">%fa:ban% %i18n:@blocking%</div>
-			<section>
-				<x-blocking/>
-			</section>
-		</ui-card>
+		<div class="muteAndBlock" v-show="page == 'muteAndBlock'">
+			<mk-mute-and-block/>
+		</div>
 
 		<ui-card class="apps" v-show="page == 'apps'">
 			<div slot="title">%fa:puzzle-piece% %i18n:@apps%</div>
@@ -297,8 +286,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import XMute from './settings.mute.vue';
-import XBlocking from './settings.blocking.vue';
 import XPassword from './settings.password.vue';
 import X2fa from './settings.2fa.vue';
 import XApps from './settings.apps.vue';
@@ -309,8 +296,6 @@ import checkForUpdate from '../../../common/scripts/check-for-update';
 
 export default Vue.extend({
 	components: {
-		XMute,
-		XBlocking,
 		XPassword,
 		X2fa,
 		XApps,
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index b96ed59311..a77f5d4c3a 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -85,6 +85,8 @@
 
 			<mk-drive-settings/>
 
+			<mk-mute-and-block/>
+
 			<ui-card>
 				<div slot="title">%fa:volume-up% %i18n:@sound%</div>
 
diff --git a/src/models/blocking.ts b/src/models/blocking.ts
index 2974b53554..3eace3989a 100644
--- a/src/models/blocking.ts
+++ b/src/models/blocking.ts
@@ -1,7 +1,12 @@
 import * as mongo from 'mongodb';
 import db from '../db/mongodb';
+import isObjectId from '../misc/is-objectid';
+const deepcopy = require('deepcopy');
+import { pack as packUser } from './user';
 
 const Blocking = db.get<IBlocking>('blocking');
+Blocking.createIndex('blockerId');
+Blocking.createIndex('blockeeId');
 Blocking.createIndex(['blockerId', 'blockeeId'], { unique: true });
 export default Blocking;
 
@@ -11,3 +16,39 @@ export type IBlocking = {
 	blockeeId: mongo.ObjectID;
 	blockerId: mongo.ObjectID;
 };
+
+export const packMany = async (
+	blockings: (string | mongo.ObjectID | IBlocking)[],
+	me?: string | mongo.ObjectID | IUser
+) => {
+	return (await Promise.all(blockings.map(x => pack(x, me)))).filter(x => x != null);
+};
+
+export const pack = (
+	blocking: any,
+	me?: any
+) => new Promise<any>(async (resolve, reject) => {
+	let _blocking: any;
+
+	// Populate the blocking if 'blocking' is ID
+	if (isObjectId(blocking)) {
+		_blocking = await Blocking.findOne({
+			_id: blocking
+		});
+	} else if (typeof blocking === 'string') {
+		_blocking = await Blocking.findOne({
+			_id: new mongo.ObjectID(blocking)
+		});
+	} else {
+		_blocking = deepcopy(blocking);
+	}
+
+	// Rename _id to id
+	_blocking.id = _blocking._id;
+	delete _blocking._id;
+
+	// Populate blockee
+	_blocking.blockee = await packUser(_blocking.blockeeId, me);
+
+	resolve(_blocking);
+});
diff --git a/src/models/mute.ts b/src/models/mute.ts
index 4bad3d3d31..58536d4b9c 100644
--- a/src/models/mute.ts
+++ b/src/models/mute.ts
@@ -1,7 +1,12 @@
 import * as mongo from 'mongodb';
 import db from '../db/mongodb';
+import isObjectId from '../misc/is-objectid';
+const deepcopy = require('deepcopy');
+import { pack as packUser } from './user';
 
 const Mute = db.get<IMute>('mute');
+Mute.createIndex('muterId');
+Mute.createIndex('muteeId');
 Mute.createIndex(['muterId', 'muteeId'], { unique: true });
 export default Mute;
 
@@ -11,3 +16,39 @@ export interface IMute {
 	muterId: mongo.ObjectID;
 	muteeId: mongo.ObjectID;
 }
+
+export const packMany = async (
+	mutes: (string | mongo.ObjectID | IMute)[],
+	me?: string | mongo.ObjectID | IUser
+) => {
+	return (await Promise.all(mutes.map(x => pack(x, me)))).filter(x => x != null);
+};
+
+export const pack = (
+	mute: any,
+	me?: any
+) => new Promise<any>(async (resolve, reject) => {
+	let _mute: any;
+
+	// Populate the mute if 'mute' is ID
+	if (isObjectId(mute)) {
+		_mute = await Mute.findOne({
+			_id: mute
+		});
+	} else if (typeof mute === 'string') {
+		_mute = await Mute.findOne({
+			_id: new mongo.ObjectID(mute)
+		});
+	} else {
+		_mute = deepcopy(mute);
+	}
+
+	// Rename _id to id
+	_mute.id = _mute._id;
+	delete _mute._id;
+
+	// Populate mutee
+	_mute.mutee = await packUser(_mute.muteeId, me);
+
+	resolve(_mute);
+});
diff --git a/src/server/api/endpoints/blocking/list.ts b/src/server/api/endpoints/blocking/list.ts
index ba1c77d2c7..a0bef38b58 100644
--- a/src/server/api/endpoints/blocking/list.ts
+++ b/src/server/api/endpoints/blocking/list.ts
@@ -1,6 +1,7 @@
 import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
-import Blocking from '../../../../models/blocking';
-import { pack, ILocalUser } from '../../../../models/user';
+import Blocking, { packMany } from '../../../../models/blocking';
+import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
 
 export const meta = {
 	desc: {
@@ -10,50 +11,54 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'following-read'
+	kind: 'following-read',
+
+	params: {
+		limit: $.num.optional.range(1, 100).note({
+			default: 30
+		}),
+
+		sinceId: $.type(ID).optional.note({
+		}),
+
+		untilId: $.type(ID).optional.note({
+		}),
+	}
 };
 
 export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'limit' parameter
-	const [limit = 30, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
-	// Get 'cursor' parameter
-	const [cursor = null, cursorErr] = $.type(ID).optional.get(params.cursor);
-	if (cursorErr) return rej('invalid cursor param');
+	// Check if both of sinceId and untilId is specified
+	if (ps.sinceId && ps.untilId) {
+		return rej('cannot set sinceId and untilId');
+	}
 
-	// Construct query
 	const query = {
 		blockerId: me._id
 	} as any;
 
-	// カーソルが指定されている場合
-	if (cursor) {
+	const sort = {
+		_id: -1
+	};
+
+	if (ps.sinceId) {
+		sort._id = 1;
 		query._id = {
-			$lt: cursor
+			$gt: ps.sinceId
+		};
+	} else if (ps.untilId) {
+		query._id = {
+			$lt: ps.untilId
 		};
 	}
 
-	// Get blockings
 	const blockings = await Blocking
 		.find(query, {
-			limit: limit + 1,
-			sort: { _id: -1 }
+			limit: ps.limit,
+			sort: sort
 		});
 
-	// 「次のページ」があるかどうか
-	const inStock = blockings.length === limit + 1;
-	if (inStock) {
-		blockings.pop();
-	}
-
-	// Serialize
-	const users = await Promise.all(blockings.map(async m =>
-		await pack(m.blockeeId, me, { detail: true })));
-
-	// Response
-	res({
-		users: users,
-		next: inStock ? blockings[blockings.length - 1]._id : null,
-	});
+	res(await packMany(blockings, me));
 });
diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts
index 387b2396f5..4653877621 100644
--- a/src/server/api/endpoints/mute/list.ts
+++ b/src/server/api/endpoints/mute/list.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
-import Mute from '../../../../models/mute';
-import { pack, ILocalUser } from '../../../../models/user';
-import { getFriendIds } from '../../common/get-friends';
+import Mute, { packMany } from '../../../../models/mute';
+import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
 
 export const meta = {
 	desc: {
@@ -11,64 +11,54 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'account/read'
+	kind: 'account/read',
+
+	params: {
+		limit: $.num.optional.range(1, 100).note({
+			default: 30
+		}),
+
+		sinceId: $.type(ID).optional.note({
+		}),
+
+		untilId: $.type(ID).optional.note({
+		}),
+	}
 };
 
 export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'iknow' parameter
-	const [iknow = false, iknowErr] = $.bool.optional.get(params.iknow);
-	if (iknowErr) return rej('invalid iknow param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
-	// Get 'limit' parameter
-	const [limit = 30, limitErr] = $.num.optional.range(1, 100).get(params.limit);
-	if (limitErr) return rej('invalid limit param');
+	// Check if both of sinceId and untilId is specified
+	if (ps.sinceId && ps.untilId) {
+		return rej('cannot set sinceId and untilId');
+	}
 
-	// Get 'cursor' parameter
-	const [cursor = null, cursorErr] = $.type(ID).optional.get(params.cursor);
-	if (cursorErr) return rej('invalid cursor param');
-
-	// Construct query
 	const query = {
-		muterId: me._id,
-		deletedAt: { $exists: false }
+		muterId: me._id
 	} as any;
 
-	if (iknow) {
-		// Get my friends
-		const myFriends = await getFriendIds(me._id);
+	const sort = {
+		_id: -1
+	};
 
-		query.muteeId = {
-			$in: myFriends
-		};
-	}
-
-	// カーソルが指定されている場合
-	if (cursor) {
+	if (ps.sinceId) {
+		sort._id = 1;
 		query._id = {
-			$lt: cursor
+			$gt: ps.sinceId
+		};
+	} else if (ps.untilId) {
+		query._id = {
+			$lt: ps.untilId
 		};
 	}
 
-	// Get mutes
 	const mutes = await Mute
 		.find(query, {
-			limit: limit + 1,
-			sort: { _id: -1 }
+			limit: ps.limit,
+			sort: sort
 		});
 
-	// 「次のページ」があるかどうか
-	const inStock = mutes.length === limit + 1;
-	if (inStock) {
-		mutes.pop();
-	}
-
-	// Serialize
-	const users = await Promise.all(mutes.map(async m =>
-		await pack(m.muteeId, me, { detail: true })));
-
-	// Response
-	res({
-		users: users,
-		next: inStock ? mutes[mutes.length - 1]._id : null,
-	});
+	res(await packMany(mutes, me));
 });