diff --git a/locales/ja.yml b/locales/ja.yml
index 3575379345..d26d13996d 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -330,6 +330,8 @@ desktop/views/components/drive.file.vue:
   banner: "バナー"
   contextmenu:
     rename: "名前を変更"
+    mark-as-sensitive: "閲覧注意に設定"
+    unmark-as-sensitive: "閲覧注意を解除"
     copy-url: "URLをコピー"
     download: "ダウンロード"
     else-files: "その他..."
@@ -377,6 +379,10 @@ desktop/views/components/drive.vue:
     upload: "ファイルをアップロード"
     url-upload: "URLからアップロード"
 
+desktop/views/components/media-image.vue:
+  sensitive: "閲覧注意"
+  click-to-show: "クリックして表示"
+
 desktop/views/components/follow-button.vue:
   following: "フォロー中"
   follow: "フォロー"
@@ -853,6 +859,10 @@ mobile/views/components/drive.file-detail.vue:
   hash: "ハッシュ (md5)"
   exif: "EXIF"
 
+mobile/views/components/media-image.vue:
+  sensitive: "閲覧注意"
+  click-to-show: "クリックして表示"
+
 mobile/views/components/follow-button.vue:
   following: "フォロー中"
   follow: "フォロー"
diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue
index 2f8a1943ad..cdfc2c8d3c 100644
--- a/src/client/app/common/views/components/media-list.vue
+++ b/src/client/app/common/views/components/media-list.vue
@@ -46,33 +46,45 @@ export default Vue.extend({
 		display grid
 		grid-gap 4px
 
+		> *
+			overflow hidden
+			border-radius 4px
+
 		&[data-count="1"]
 			grid-template-rows 1fr
+
 		&[data-count="2"]
 			grid-template-columns 1fr 1fr
 			grid-template-rows 1fr
+
 		&[data-count="3"]
 			grid-template-columns 1fr 0.5fr
 			grid-template-rows 1fr 1fr
-			:nth-child(1)
+
+			> *:nth-child(1)
 				grid-row 1 / 3
-			:nth-child(3)
+
+			> *:nth-child(3)
 				grid-column 2 / 3
 				grid-row 2 / 3
+
 		&[data-count="4"]
 			grid-template-columns 1fr 1fr
 			grid-template-rows 1fr 1fr
 
-		:nth-child(1)
+		> *:nth-child(1)
 			grid-column 1 / 2
 			grid-row 1 / 2
-		:nth-child(2)
+
+		> *:nth-child(2)
 			grid-column 2 / 3
 			grid-row 1 / 2
-		:nth-child(3)
+
+		> *:nth-child(3)
 			grid-column 1 / 2
 			grid-row 2 / 3
-		:nth-child(4)
+
+		> *:nth-child(4)
 			grid-column 2 / 3
 			grid-row 2 / 3
 
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
index 86addb1318..11700d4966 100644
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ b/src/client/app/desktop/views/components/drive.file.vue
@@ -68,6 +68,11 @@ export default Vue.extend({
 				icon: '%fa:i-cursor%',
 				action: this.rename
 			}, {
+				type: 'item',
+				text: this.file.isSensitive ? '%i18n:@contextmenu.unmark-as-sensitive%' : '%i18n:@contextmenu.mark-as-sensitive%',
+				icon: this.file.isSensitive ? '%fa:R eye%' : '%fa:R eye-slash%',
+				action: this.toggleSensitive
+			}, null, {
 				type: 'item',
 				text: '%i18n:@contextmenu.copy-url%',
 				icon: '%fa:link%',
@@ -149,6 +154,13 @@ export default Vue.extend({
 			});
 		},
 
+		toggleSensitive() {
+			(this as any).api('drive/files/update', {
+				fileId: this.file.id,
+				isSensitive: !this.file.isSensitive
+			});
+		},
+
 		copyUrl() {
 			copyToClipboard(this.file.url);
 			(this as any).apis.dialog({
diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue
index b98a4707ec..42a31c4c2d 100644
--- a/src/client/app/desktop/views/components/media-image.vue
+++ b/src/client/app/desktop/views/components/media-image.vue
@@ -1,5 +1,11 @@
 <template>
-<a class="mk-media-image"
+<div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide" @click="hide = false">
+	<div>
+		<b>%fa:exclamation-triangle% %i18n:@sensitive%</b>
+		<span>%i18n:@click-to-show%</span>
+	</div>
+</div>
+<a class="lcjomzwbohoelkxsnuqjiaccdbdfiazy" v-else
 	:href="image.url"
 	@mousemove="onMousemove"
 	@mouseleave="onMouseleave"
@@ -21,6 +27,10 @@ export default Vue.extend({
 		},
 		raw: {
 			default: false
+		},
+		hide: {
+			type: Boolean,
+			default: true
 		}
 	},
 	computed: {
@@ -56,16 +66,30 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-media-image
+.lcjomzwbohoelkxsnuqjiaccdbdfiazy
 	display block
 	cursor zoom-in
 	overflow hidden
 	width 100%
 	height 100%
 	background-position center
-	border-radius 4px
 
 	&:not(:hover)
 		background-size cover
 
+.ldwbgwstjsdgcjruamauqdrffetqudry
+	display flex
+	justify-content center
+	align-items center
+	background #111
+	color #fff
+
+	> div
+		display table-cell
+		text-align center
+		font-size 12px
+
+		> b
+			display block
+
 </style>
diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue
index c2f9c66e84..1042404c98 100644
--- a/src/client/app/mobile/views/components/media-image.vue
+++ b/src/client/app/mobile/views/components/media-image.vue
@@ -1,5 +1,11 @@
 <template>
-<a class="mk-media-image" :href="image.url" target="_blank" :style="style" :title="image.name"></a>
+<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide" @click="hide = false">
+	<div>
+		<b>%fa:exclamation-triangle% %i18n:@sensitive%</b>
+		<span>%i18n:@click-to-show%</span>
+	</div>
+</div>
+<a class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else :href="image.url" target="_blank" :style="style" :title="image.name"></a>
 </template>
 
 <script lang="ts">
@@ -13,6 +19,10 @@ export default Vue.extend({
 		},
 		raw: {
 			default: false
+		},
+		hide: {
+			type: Boolean,
+			default: true
 		}
 	},
 	computed: {
@@ -35,13 +45,27 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-media-image
+.gqnyydlzavusgskkfvwvjiattxdzsqlf
 	display block
 	overflow hidden
 	width 100%
 	height 100%
 	background-position center
 	background-size cover
-	border-radius 4px
+
+.qjewsnkgzzxlxtzncydssfbgjibiehcy
+	display flex
+	justify-content center
+	align-items center
+	background #111
+	color #fff
+
+	> div
+		display table-cell
+		text-align center
+		font-size 12px
+
+		> b
+			display block
 
 </style>
diff --git a/src/docs/api/entities/drive-file.yaml b/src/docs/api/entities/drive-file.yaml
index bb39e90112..2d14c6e1f5 100644
--- a/src/docs/api/entities/drive-file.yaml
+++ b/src/docs/api/entities/drive-file.yaml
@@ -81,3 +81,10 @@ props:
     desc:
       ja: "フォルダ"
       en: "The folder of this file"
+
+  sensitive:
+    type: "boolean"
+    optional: true
+    desc:
+      ja: "このメディアが「閲覧注意」(NSFW)かどうか"
+      en: "Whether this media is NSFW"
diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
index 2bdf38f484..3a0390f792 100644
--- a/src/models/drive-file.ts
+++ b/src/models/drive-file.ts
@@ -33,6 +33,7 @@ export type IMetadata = {
 	url?: string;
 	deletedAt?: Date;
 	isMetaOnly?: boolean;
+	isSensitive?: boolean;
 };
 
 export type IDriveFile = {
diff --git a/src/remote/activitypub/models/image.ts b/src/remote/activitypub/models/image.ts
index fb17a7c9e5..8b33187ef5 100644
--- a/src/remote/activitypub/models/image.ts
+++ b/src/remote/activitypub/models/image.ts
@@ -16,7 +16,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv
 		return null;
 	}
 
-	const image = await new Resolver().resolve(value);
+	const image = await new Resolver().resolve(value) as any;
 
 	if (image.url == null) {
 		throw new Error('invalid image: url not privided');
@@ -24,7 +24,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv
 
 	log(`Creating the Image: ${image.url}`);
 
-	return await uploadFromUrl(image.url, actor, null, image.url);
+	return await uploadFromUrl(image.url, actor, null, image.url, image.sensitive);
 }
 
 /**
diff --git a/src/remote/activitypub/renderer/image.ts b/src/remote/activitypub/renderer/image.ts
index cf91ce3a0c..69bddd9188 100644
--- a/src/remote/activitypub/renderer/image.ts
+++ b/src/remote/activitypub/renderer/image.ts
@@ -1,7 +1,8 @@
 import config from '../../../config';
 import { IDriveFile } from '../../../models/drive-file';
 
-export default (fileId: IDriveFile['_id']) => ({
+export default (file: IDriveFile) => ({
 	type: 'Image',
-	url: `${config.drive_url}/${fileId}`
+	url: `${config.drive_url}/${file._id}`,
+	sensitive: file.metadata.isSensitive
 });
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
index d4b3f40e41..7d828f97ae 100644
--- a/src/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -4,10 +4,16 @@ import config from '../../../config';
 import { ILocalUser } from '../../../models/user';
 import toHtml from '../../../mfm/html';
 import parse from '../../../mfm/parse';
+import DriveFile from '../../../models/drive-file';
 
-export default (user: ILocalUser) => {
+export default async (user: ILocalUser) => {
 	const id = `${config.url}/users/${user._id}`;
 
+	const [avatar, banner] = await Promise.all([
+		DriveFile.findOne({ _id: user.avatarId }),
+		DriveFile.findOne({ _id: user.bannerId })
+	]);
+
 	return {
 		type: user.isBot ? 'Service' : 'Person',
 		id,
@@ -18,8 +24,8 @@ export default (user: ILocalUser) => {
 		preferredUsername: user.username,
 		name: user.name,
 		summary: toHtml(parse(user.description)),
-		icon: user.avatarId && renderImage(user.avatarId),
-		image: user.bannerId && renderImage(user.bannerId),
+		icon: user.avatarId && renderImage(avatar),
+		image: user.bannerId && renderImage(banner),
 		manuallyApprovesFollowers: user.isLocked,
 		publicKey: renderKey(user)
 	};
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index 0448ae61b8..17cd34ee6f 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -111,13 +111,13 @@ router.get('/users/:user/publickey', async ctx => {
 });
 
 // user
-function userInfo(ctx: Router.IRouterContext, user: IUser) {
+async function userInfo(ctx: Router.IRouterContext, user: IUser) {
 	if (user === null) {
 		ctx.status = 404;
 		return;
 	}
 
-	ctx.body = pack(renderPerson(user as ILocalUser));
+	ctx.body = pack(await renderPerson(user as ILocalUser));
 }
 
 router.get('/users/:user', async ctx => {
@@ -128,7 +128,7 @@ router.get('/users/:user', async ctx => {
 		host: null
 	});
 
-	userInfo(ctx, user);
+	await userInfo(ctx, user);
 });
 
 router.get('/@:user', async (ctx, next) => {
@@ -139,7 +139,7 @@ router.get('/@:user', async (ctx, next) => {
 		host: null
 	});
 
-	userInfo(ctx, user);
+	await userInfo(ctx, user);
 });
 //#endregion
 
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
index ca12be9104..1c5506f6c4 100644
--- a/src/server/api/endpoints/drive/files/create.ts
+++ b/src/server/api/endpoints/drive/files/create.ts
@@ -29,6 +29,14 @@ export const meta = {
 			desc: {
 				ja: 'フォルダID'
 			}
+		}),
+
+		isSensitive: $.bool.optional.note({
+			default: false,
+			desc: {
+				ja: 'このメディアが「閲覧注意」(NSFW)かどうか',
+				en: 'Whether this media is NSFW'
+			}
 		})
 	}
 };
@@ -68,7 +76,7 @@ export default async (file: any, params: any, user: ILocalUser): Promise<any> =>
 
 	try {
 		// Create file
-		const driveFile = await create(user, file.path, name, null, ps.folderId);
+		const driveFile = await create(user, file.path, name, null, ps.folderId, false, false, null, null, ps.isSensitive);
 
 		cleanup();
 
diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts
index 396bc97694..bac04bae78 100644
--- a/src/server/api/endpoints/drive/files/update.ts
+++ b/src/server/api/endpoints/drive/files/update.ts
@@ -3,6 +3,7 @@ import DriveFolder from '../../../../../models/drive-folder';
 import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file';
 import { publishDriveStream } from '../../../../../stream';
 import { ILocalUser } from '../../../../../models/user';
+import getParams from '../../../get-params';
 
 export const meta = {
 	desc: {
@@ -12,18 +13,48 @@ export const meta = {
 
 	requireCredential: true,
 
-	kind: 'drive-write'
+	kind: 'drive-write',
+
+	params: {
+		fileId: $.type(ID).note({
+			desc: {
+				ja: '対象のファイルID'
+			}
+		}),
+
+		folderId: $.type(ID).optional.nullable.note({
+			default: undefined,
+			desc: {
+				ja: 'フォルダID'
+			}
+		}),
+
+		name: $.str.optional.pipe(validateFileName).note({
+			default: undefined,
+			desc: {
+				ja: 'ファイル名',
+				en: 'Name of the file'
+			}
+		}),
+
+		isSensitive: $.bool.optional.note({
+			default: undefined,
+			desc: {
+				ja: 'このメディアが「閲覧注意」(NSFW)かどうか',
+				en: 'Whether this media is NSFW'
+			}
+		})
+	}
 };
 
 export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
-	// Get 'fileId' parameter
-	const [fileId, fileIdErr] = $.type(ID).get(params.fileId);
-	if (fileIdErr) return rej('invalid fileId param');
+	const [ps, psErr] = getParams(meta, params);
+	if (psErr) return rej(psErr);
 
 	// Fetch file
 	const file = await DriveFile
 		.findOne({
-			_id: fileId,
+			_id: ps.fileId,
 			'metadata.userId': user._id
 		});
 
@@ -31,23 +62,18 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		return rej('file-not-found');
 	}
 
-	// Get 'name' parameter
-	const [name, nameErr] = $.str.optional.pipe(validateFileName).get(params.name);
-	if (nameErr) return rej('invalid name param');
-	if (name) file.filename = name;
+	if (ps.name) file.filename = ps.name;
 
-	// Get 'folderId' parameter
-	const [folderId, folderIdErr] = $.type(ID).optional.nullable.get(params.folderId);
-	if (folderIdErr) return rej('invalid folderId param');
+	if (ps.isSensitive) file.metadata.isSensitive = ps.isSensitive;
 
-	if (folderId !== undefined) {
-		if (folderId === null) {
+	if (ps.folderId !== undefined) {
+		if (ps.folderId === null) {
 			file.metadata.folderId = null;
 		} else {
 			// Fetch folder
 			const folder = await DriveFolder
 				.findOne({
-					_id: folderId,
+					_id: ps.folderId,
 					userId: user._id
 				});
 
@@ -62,7 +88,8 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 	await DriveFile.update(file._id, {
 		$set: {
 			filename: file.filename,
-			'metadata.folderId': file.metadata.folderId
+			'metadata.folderId': file.metadata.folderId,
+			'metadata.isSensitive': file.metadata.isSensitive
 		}
 	});
 
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index 57c6589176..73d5b4962c 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -83,7 +83,8 @@ export default async function(
 	force: boolean = false,
 	metaOnly: boolean = false,
 	url: string = null,
-	uri: string = null
+	uri: string = null,
+	sensitive = false
 ): Promise<IDriveFile> {
 	// Calc md5 hash
 	const calcHash = new Promise<string>((res, rej) => {
@@ -258,7 +259,8 @@ export default async function(
 		folderId: folder !== null ? folder._id : null,
 		comment: comment,
 		properties: properties,
-		isMetaOnly: metaOnly
+		isMetaOnly: metaOnly,
+		isSensitive: sensitive
 	} as IMetadata;
 
 	if (url !== null) {
diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts
index 711889ea41..4e297d3bb1 100644
--- a/src/services/drive/upload-from-url.ts
+++ b/src/services/drive/upload-from-url.ts
@@ -13,7 +13,7 @@ import * as mongodb from 'mongodb';
 
 const log = debug('misskey:drive:upload-from-url');
 
-export default async (url: string, user: IUser, folderId: mongodb.ObjectID = null, uri: string = null): Promise<IDriveFile> => {
+export default async (url: string, user: IUser, folderId: mongodb.ObjectID = null, uri: string = null, sensitive = false): Promise<IDriveFile> => {
 	log(`REQUESTED: ${url}`);
 
 	let name = URL.parse(url).pathname.split('/').pop();
@@ -48,7 +48,7 @@ export default async (url: string, user: IUser, folderId: mongodb.ObjectID = nul
 	let error;
 
 	try {
-		driveFile = await create(user, path, name, null, folderId, false, config.preventCacheRemoteFiles, url, uri);
+		driveFile = await create(user, path, name, null, folderId, false, config.preventCacheRemoteFiles, url, uri, sensitive);
 		log(`got: ${driveFile._id}`);
 	} catch (e) {
 		error = e;