From 47ce59d555eab91b0642b4e4b62c8ecc4ff2288a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 28 Apr 2018 17:25:56 +0900
Subject: [PATCH 01/12] wip

---
 .../views/components/visibility-chooser.vue   | 213 ++++++++++++++++++
 .../desktop/views/components/post-form.vue    |  32 +--
 src/models/note.ts                            |  11 +-
 .../activitypub/kernel/announce/note.ts       |   4 +-
 src/remote/activitypub/models/note.ts         |   4 +-
 5 files changed, 244 insertions(+), 20 deletions(-)
 create mode 100644 src/client/app/common/views/components/visibility-chooser.vue

diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
new file mode 100644
index 000000000..71e92a85f
--- /dev/null
+++ b/src/client/app/common/views/components/visibility-chooser.vue
@@ -0,0 +1,213 @@
+<template>
+<div class="mk-visibility-chooser">
+	<div class="backdrop" ref="backdrop" @click="close"></div>
+	<div class="popover" :class="{ compact }" ref="popover">
+		<div @click="choose('public')" :class="{ active: v == 'public' }">
+			<div>%fa:globe%</div>
+			<div>
+				<span>公開</span>
+			</div>
+		</div>
+		<div @click="choose('home')" :class="{ active: v == 'home' }">
+			<div>%fa:home%</div>
+			<div>
+				<span>ホーム</span>
+				<span>ホームタイムラインにのみ公開</span>
+			</div>
+		</div>
+		<div @click="choose('followers')" :class="{ active: v == 'followers' }">
+			<div>%fa:unlock%</div>
+			<div>
+				<span>フォロワー</span>
+				<span>自分のフォロワーのみに公開</span>
+			</div>
+		</div>
+		<div @click="choose('mentioned')" :class="{ active: v == 'mentioned' }">
+			<div>%fa:envelope%</div>
+			<div>
+				<span>メンション</span>
+				<span>言及したユーザーのみに公開</span>
+			</div>
+		</div>
+		<div @click="choose('private')" :class="{ active: v == 'private' }">
+			<div>%fa:lock%</div>
+			<div>
+				<span>非公開</span>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+
+export default Vue.extend({
+	props: ['source', 'compact', 'v'],
+	mounted() {
+		this.$nextTick(() => {
+			const popover = this.$refs.popover as any;
+
+			const rect = this.source.getBoundingClientRect();
+			const width = popover.offsetWidth;
+			const height = popover.offsetHeight;
+
+			if (this.compact) {
+				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+				const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
+				popover.style.left = (x - (width / 2)) + 'px';
+				popover.style.top = (y - (height / 2)) + 'px';
+			} else {
+				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+				const y = rect.top + window.pageYOffset + this.source.offsetHeight;
+				popover.style.left = (x - (width / 2)) + 'px';
+				popover.style.top = y + 'px';
+			}
+
+			anime({
+				targets: this.$refs.backdrop,
+				opacity: 1,
+				duration: 100,
+				easing: 'linear'
+			});
+
+			anime({
+				targets: this.$refs.popover,
+				opacity: 1,
+				scale: [0.5, 1],
+				duration: 500
+			});
+		});
+	},
+	methods: {
+		choose(visibility) {
+			this.$emit('chosen', visibility);
+			this.$destroy();
+		},
+		close() {
+			(this.$refs.backdrop as any).style.pointerEvents = 'none';
+			anime({
+				targets: this.$refs.backdrop,
+				opacity: 0,
+				duration: 200,
+				easing: 'linear'
+			});
+
+			(this.$refs.popover as any).style.pointerEvents = 'none';
+			anime({
+				targets: this.$refs.popover,
+				opacity: 0,
+				scale: 0.5,
+				duration: 200,
+				easing: 'easeInBack',
+				complete: () => this.$destroy()
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+$border-color = rgba(27, 31, 35, 0.15)
+
+root(isDark)
+	position initial
+
+	> .backdrop
+		position fixed
+		top 0
+		left 0
+		z-index 10000
+		width 100%
+		height 100%
+		background isDark ? rgba(#000, 0.4) : rgba(#000, 0.1)
+		opacity 0
+
+	> .popover
+		$bgcolor = isDark ? #2c303c : #fff
+		position absolute
+		z-index 10001
+		width 240px
+		padding 8px 0
+		background $bgcolor
+		border 1px solid $border-color
+		border-radius 4px
+		box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
+		transform scale(0.5)
+		opacity 0
+
+		$balloon-size = 10px
+
+		&:not(.compact)
+			margin-top $balloon-size
+			transform-origin center -($balloon-size)
+
+			&:before
+				content ""
+				display block
+				position absolute
+				top -($balloon-size * 2)
+				left s('calc(50% - %s)', $balloon-size)
+				border-top solid $balloon-size transparent
+				border-left solid $balloon-size transparent
+				border-right solid $balloon-size transparent
+				border-bottom solid $balloon-size $border-color
+
+			&:after
+				content ""
+				display block
+				position absolute
+				top -($balloon-size * 2) + 1.5px
+				left s('calc(50% - %s)', $balloon-size)
+				border-top solid $balloon-size transparent
+				border-left solid $balloon-size transparent
+				border-right solid $balloon-size transparent
+				border-bottom solid $balloon-size $bgcolor
+
+		> div
+			display flex
+			padding 8px 14px
+			font-size 12px
+			color isDark ? #fff : #666
+			cursor pointer
+
+			&:hover
+				background isDark ? #252731 : #eee
+
+			&:active
+				background isDark ? #21242b : #ddd
+
+			&.active
+				cursor $theme-color-foreground
+				background $theme-color
+
+			> *
+				user-select none
+				pointer-events none
+
+			> *:first-child
+				display flex
+				justify-content center
+				align-items center
+				margin-right 10px
+
+			> *:last-child
+				flex 1 1 auto
+
+				> span:first-child
+					display block
+					font-weight bold
+
+				> span:last-child:not(:first-child)
+					opacity 0.6
+
+.mk-visibility-chooser[data-darkmode]
+	root(true)
+
+.mk-visibility-chooser:not([data-darkmode])
+	root(false)
+
+</style>
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index 80d9d6675..d1d7076a3 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -30,7 +30,7 @@
 	<button class="poll" title="%i18n:@create-poll%" @click="poll = true">%fa:chart-pie%</button>
 	<button class="poll" title="内容を隠す" @click="useCw = !useCw">%fa:eye-slash%</button>
 	<button class="geo" title="位置情報を添付する" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
-	<p class="text-count" :class="{ over: text.length > 1000 }">{{ '%i18n:!@text-remain%'.replace('{}', 1000 - text.length) }}</p>
+	<button class="visibility" title="公開範囲" @click="setVisibility" ref="visibilityButton">%fa:lock%</button>
 	<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
 		{{ posting ? '%i18n:!@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
 	</button>
@@ -43,10 +43,12 @@
 import Vue from 'vue';
 import * as XDraggable from 'vuedraggable';
 import getKao from '../../../common/scripts/get-kao';
+import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
 
 export default Vue.extend({
 	components: {
-		XDraggable
+		XDraggable,
+		MkVisibilityChooser
 	},
 
 	props: ['reply', 'renote'],
@@ -61,6 +63,7 @@ export default Vue.extend({
 			useCw: false,
 			cw: null,
 			geo: null,
+			visibility: 'public',
 			autocomplete: null,
 			draghover: false
 		};
@@ -246,6 +249,16 @@ export default Vue.extend({
 			this.$emit('geo-dettached');
 		},
 
+		setVisibility() {
+			const w = (this as any).os.new(MkVisibilityChooser, {
+				source: this.$refs.visibilityButton,
+				v: this.visibility
+			});
+			w.$once('chosen', v => {
+				this.visibility = v;
+			});
+		},
+
 		post() {
 			this.posting = true;
 
@@ -256,6 +269,7 @@ export default Vue.extend({
 				renoteId: this.renote ? this.renote.id : undefined,
 				poll: this.poll ? (this.$refs.poll as any).get() : undefined,
 				cw: this.useCw ? this.cw || '' : undefined,
+				visibility: this.visibility,
 				geo: this.geo ? {
 					coordinates: [this.geo.longitude, this.geo.latitude],
 					altitude: this.geo.altitude,
@@ -450,19 +464,6 @@ root(isDark)
 	input[type='file']
 		display none
 
-	.text-count
-		pointer-events none
-		display block
-		position absolute
-		bottom 16px
-		right 138px
-		margin 0
-		line-height 40px
-		color rgba($theme-color, 0.5)
-
-		&.over
-			color #ec3828
-
 	.submit
 		display block
 		position absolute
@@ -532,6 +533,7 @@ root(isDark)
 	> .kao
 	> .poll
 	> .geo
+	> .visibility
 		display inline-block
 		cursor pointer
 		padding 0
diff --git a/src/models/note.ts b/src/models/note.ts
index 3c835ed19..2f95cbfd6 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -46,7 +46,16 @@ export type INote = {
 	repliesCount: number;
 	reactionCounts: any;
 	mentions: mongo.ObjectID[];
-	visibility: 'public' | 'unlisted' | 'private' | 'direct';
+
+	/**
+	 * public ... 公開
+	 * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す
+	 * followers ... フォロワーのみ
+	 * mentioned ... 言及したユーザーのみ
+	 * private ... 自分のみ
+	 */
+	visibility: 'public' | 'home' | 'followers' | 'mentioned' | 'private';
+
 	geo: {
 		coordinates: number[];
 		altitude: number;
diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts
index a288dd499..e2f3806d7 100644
--- a/src/remote/activitypub/kernel/announce/note.ts
+++ b/src/remote/activitypub/kernel/announce/note.ts
@@ -30,8 +30,8 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
 
 	//#region Visibility
 	let visibility = 'public';
-	if (!activity.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted';
-	if (activity.cc.length == 0) visibility = 'private';
+	if (!activity.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'home';
+	if (activity.cc.length == 0) visibility = 'followers';
 	// TODO
 	if (visibility != 'public') throw new Error('unspported visibility');
 	//#endergion
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index f830370a2..c0f67cb2f 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -65,8 +65,8 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 
 	//#region Visibility
 	let visibility = 'public';
-	if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted';
-	if (note.cc.length == 0) visibility = 'private';
+	if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'home';
+	if (note.cc.length == 0) visibility = 'followers';
 	// TODO
 	if (visibility != 'public') return null;
 	//#endergion

From 6cb520cd2cedcd51eae3adafe21f7f45ce446b77 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 28 Apr 2018 17:27:41 +0900
Subject: [PATCH 02/12] oops

---
 src/client/app/common/views/components/visibility-chooser.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
index 71e92a85f..e37717f2c 100644
--- a/src/client/app/common/views/components/visibility-chooser.vue
+++ b/src/client/app/common/views/components/visibility-chooser.vue
@@ -181,7 +181,7 @@ root(isDark)
 				background isDark ? #21242b : #ddd
 
 			&.active
-				cursor $theme-color-foreground
+				color $theme-color-foreground
 				background $theme-color
 
 			> *

From 34786130b75856f67a3534cae1eef583ca5ec2f7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 28 Apr 2018 17:29:46 +0900
Subject: [PATCH 03/12] :v:

---
 src/client/app/common/views/components/visibility-chooser.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
index e37717f2c..b6a8e9b66 100644
--- a/src/client/app/common/views/components/visibility-chooser.vue
+++ b/src/client/app/common/views/components/visibility-chooser.vue
@@ -19,14 +19,14 @@
 			<div>%fa:unlock%</div>
 			<div>
 				<span>フォロワー</span>
-				<span>自分のフォロワーのみに公開</span>
+				<span>自分のフォロワーにのみ公開</span>
 			</div>
 		</div>
 		<div @click="choose('mentioned')" :class="{ active: v == 'mentioned' }">
 			<div>%fa:envelope%</div>
 			<div>
 				<span>メンション</span>
-				<span>言及したユーザーのみに公開</span>
+				<span>言及したユーザーにのみ公開</span>
 			</div>
 		</div>
 		<div @click="choose('private')" :class="{ active: v == 'private' }">

From ec4ed8fb2d87f02ff67cfbf6b9f8e2975bc49e4f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 29 Apr 2018 04:30:51 +0900
Subject: [PATCH 04/12] wip

---
 .../views/components/visibility-chooser.vue   |  6 +--
 src/models/note.ts                            | 53 ++++++++++++++++++-
 src/server/api/endpoints/notes/create.ts      | 16 +++++-
 src/services/note/create.ts                   |  6 +++
 4 files changed, 74 insertions(+), 7 deletions(-)

diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
index b6a8e9b66..dd36d32e7 100644
--- a/src/client/app/common/views/components/visibility-chooser.vue
+++ b/src/client/app/common/views/components/visibility-chooser.vue
@@ -22,11 +22,11 @@
 				<span>自分のフォロワーにのみ公開</span>
 			</div>
 		</div>
-		<div @click="choose('mentioned')" :class="{ active: v == 'mentioned' }">
+		<div @click="choose('specified')" :class="{ active: v == 'specified' }">
 			<div>%fa:envelope%</div>
 			<div>
-				<span>メンション</span>
-				<span>言及したユーザーにのみ公開</span>
+				<span>ダイレクト</span>
+				<span>指定したユーザーにのみ公開</span>
 			</div>
 		</div>
 		<div @click="choose('private')" :class="{ active: v == 'private' }">
diff --git a/src/models/note.ts b/src/models/note.ts
index 2f95cbfd6..5c4ac8635 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -12,6 +12,7 @@ import NoteWatching, { deleteNoteWatching } from './note-watching';
 import NoteReaction from './note-reaction';
 import Favorite, { deleteFavorite } from './favorite';
 import Notification, { deleteNotification } from './notification';
+import Following from './following';
 
 const Note = db.get<INote>('notes');
 
@@ -51,10 +52,12 @@ export type INote = {
 	 * public ... 公開
 	 * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す
 	 * followers ... フォロワーのみ
-	 * mentioned ... 言及したユーザーのみ
+	 * specified ... visibleUserIds で指定したユーザーのみ
 	 * private ... 自分のみ
 	 */
-	visibility: 'public' | 'home' | 'followers' | 'mentioned' | 'private';
+	visibility: 'public' | 'home' | 'followers' | 'specified' | 'private';
+
+	visibleUserIds: mongo.ObjectID[];
 
 	geo: {
 		coordinates: number[];
@@ -190,6 +193,52 @@ export const pack = async (
 
 	if (!_note) throw `invalid note arg ${note}`;
 
+	let hide = false;
+
+	// visibility が private かつ投稿者のIDが自分のIDではなかったら非表示
+	if (_note.visibility == 'private' && (meId == null || !meId.equals(_note.userId))) {
+		hide = true;
+	}
+
+	// visibility が specified かつ自分が指定されていなかったら非表示
+	if (_note.visibility == 'specified') {
+		if (meId == null) {
+			hide = true;
+		} else if (meId.equals(_note.userId)) {
+			hide = false;
+		} else {
+			// 指定されているかどうか
+			const specified = _note.visibleUserIds.test(id => id.equals(meId));
+
+			if (specified) {
+				hide = false;
+			} else {
+				hide = true;
+			}
+		}
+	}
+
+	// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
+	if (_note.visibility == 'followers') {
+		if (meId == null) {
+			hide = true;
+		} else if (meId.equals(_note.userId)) {
+			hide = false;
+		} else {
+			// フォロワーかどうか
+			const following = await Following.findOne({
+				followeeId: _note.userId,
+				followerId: meId
+			});
+
+			if (following == null) {
+				hide = true;
+			} else {
+				hide = false;
+			}
+		}
+	}
+
 	const id = _note._id;
 
 	// Rename _id to id
diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts
index af4f36522..52c6068fd 100644
--- a/src/server/api/endpoints/notes/create.ts
+++ b/src/server/api/endpoints/notes/create.ts
@@ -3,7 +3,7 @@
  */
 import $ from 'cafy'; import ID from '../../../../cafy-id';
 import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note';
-import { ILocalUser } from '../../../../models/user';
+import User, { ILocalUser } from '../../../../models/user';
 import Channel, { IChannel } from '../../../../models/channel';
 import DriveFile from '../../../../models/drive-file';
 import create from '../../../../services/note/create';
@@ -14,9 +14,20 @@ import { IApp } from '../../../../models/app';
  */
 module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => {
 	// Get 'visibility' parameter
-	const [visibility = 'public', visibilityErr] = $(params.visibility).optional.string().or(['public', 'unlisted', 'private', 'direct']).get();
+	const [visibility = 'public', visibilityErr] = $(params.visibility).optional.string().or(['public', 'home', 'followers', 'specified', 'private']).get();
 	if (visibilityErr) return rej('invalid visibility');
 
+	// Get 'visibleUserIds' parameter
+	const [visibleUserIds, visibleUserIdsErr] = $(params.visibleUserIds).optional.array($().type(ID)).unique().min(1).get();
+	if (visibleUserIdsErr) return rej('invalid visibleUserIds');
+
+	let visibleUsers = [];
+	if (visibleUserIds !== undefined) {
+		visibleUsers = await Promise.all(visibleUserIds.map(id => User.findOne({
+			_id: id
+		})));
+	}
+
 	// Get 'text' parameter
 	const [text = null, textErr] = $(params.text).optional.nullable.string().pipe(isValidText).get();
 	if (textErr) return rej('invalid text');
@@ -191,6 +202,7 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res
 		app,
 		viaMobile,
 		visibility,
+		visibleUsers,
 		geo
 	});
 
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 4808edfda..e8070595c 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -30,6 +30,7 @@ export default async (user: IUser, data: {
 	tags?: string[];
 	cw?: string;
 	visibility?: string;
+	visibleUsers?: IUser[];
 	uri?: string;
 	app?: IApp;
 }, silent = false) => new Promise<INote>(async (res, rej) => {
@@ -57,6 +58,10 @@ export default async (user: IUser, data: {
 		});
 	}
 
+	if (data.visibleUsers) {
+		data.visibleUsers = data.visibleUsers.filter(x => x != null);
+	}
+
 	const insert: any = {
 		createdAt: data.createdAt,
 		mediaIds: data.media ? data.media.map(file => file._id) : [],
@@ -71,6 +76,7 @@ export default async (user: IUser, data: {
 		geo: data.geo || null,
 		appId: data.app ? data.app._id : null,
 		visibility: data.visibility,
+		visibleUserIds: data.visibleUsers ? data.visibleUsers.map(u => u._id) : [],
 
 		// 以下非正規化データ
 		_reply: data.reply ? { userId: data.reply.userId } : null,

From 671c5e7c12feb187c17633baa239bddd2d92a6c1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 29 Apr 2018 04:44:58 +0900
Subject: [PATCH 05/12] wip

---
 src/remote/activitypub/kernel/announce/note.ts | 15 +++++++++++----
 src/remote/activitypub/models/note.ts          | 13 ++++++++++---
 2 files changed, 21 insertions(+), 7 deletions(-)

diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts
index e2f3806d7..fe645b07b 100644
--- a/src/remote/activitypub/kernel/announce/note.ts
+++ b/src/remote/activitypub/kernel/announce/note.ts
@@ -5,6 +5,7 @@ import post from '../../../../services/note/create';
 import { IRemoteUser } from '../../../../models/user';
 import { IAnnounce, INote } from '../../type';
 import { fetchNote, resolveNote } from '../../models/note';
+import { resolvePerson } from '../../models/person';
 
 const log = debug('misskey:activitypub');
 
@@ -30,16 +31,22 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
 
 	//#region Visibility
 	let visibility = 'public';
-	if (!activity.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'home';
-	if (activity.cc.length == 0) visibility = 'followers';
-	// TODO
-	if (visibility != 'public') throw new Error('unspported visibility');
+	let visibleUsers = [];
+	if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
+		if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
+			visibility = 'home';
+		} else {
+			visibility = 'specified';
+			visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri)));
+		}
+	}	if (activity.cc.length == 0) visibility = 'followers';
 	//#endergion
 
 	await post(actor, {
 		createdAt: new Date(activity.published),
 		renote,
 		visibility,
+		visibleUsers,
 		uri
 	});
 }
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index c0f67cb2f..9a0cc4e0c 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -65,10 +65,16 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 
 	//#region Visibility
 	let visibility = 'public';
-	if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'home';
+	let visibleUsers = [];
+	if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
+		if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
+			visibility = 'home';
+		} else {
+			visibility = 'specified';
+			visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri)));
+		}
+	}
 	if (note.cc.length == 0) visibility = 'followers';
-	// TODO
-	if (visibility != 'public') return null;
 	//#endergion
 
 	// 添付メディア
@@ -99,6 +105,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 		viaMobile: false,
 		geo: undefined,
 		visibility,
+		visibleUsers,
 		uri: note.id
 	}, silent);
 }

From 6df010a71e4be1ee819c0c2730334a6c0f5b4387 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 29 Apr 2018 04:51:19 +0900
Subject: [PATCH 06/12] wip

---
 src/models/note.ts | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/src/models/note.ts b/src/models/note.ts
index 5c4ac8635..918ef6d69 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -262,7 +262,7 @@ export const pack = async (
 	}
 
 	// Populate media
-	if (_note.mediaIds) {
+	if (_note.mediaIds && !hide) {
 		_note.media = Promise.all(_note.mediaIds.map(fileId =>
 			packFile(fileId)
 		));
@@ -321,7 +321,7 @@ export const pack = async (
 		}
 
 		// Poll
-		if (meId && _note.poll) {
+		if (meId && _note.poll && !hide) {
 			_note.poll = (async (poll) => {
 				const vote = await PollVote
 					.findOne({
@@ -362,5 +362,12 @@ export const pack = async (
 	// resolve promises in _note object
 	_note = await rap(_note);
 
+	if (hide) {
+		_note.mediaIds = [];
+		_note.text = null;
+		_note.poll = null;
+		_note.isHidden = true;
+	}
+
 	return _note;
 };

From 7797a0746a665673db60fae7ed82329153d21262 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 29 Apr 2018 05:28:34 +0900
Subject: [PATCH 07/12] wip

---
 .../desktop/views/components/post-form.vue    | 30 +++++++++++++++++++
 src/services/note/create.ts                   |  6 +++-
 2 files changed, 35 insertions(+), 1 deletion(-)

diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index d1d7076a3..a9ce1354d 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -6,6 +6,10 @@
 	@drop.stop="onDrop"
 >
 	<div class="content">
+		<div v-if="visibility == 'specified'" class="visibleUsers">
+			<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
+			<a @click="addVisibleUser">+ユーザーを追加</a>
+		</div>
 		<input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)">
 		<textarea :class="{ with: (files.length != 0 || poll) }"
 			ref="text" v-model="text" :disabled="posting"
@@ -64,6 +68,7 @@ export default Vue.extend({
 			cw: null,
 			geo: null,
 			visibility: 'public',
+			visibleUsers: [],
 			autocomplete: null,
 			draghover: false
 		};
@@ -259,6 +264,22 @@ export default Vue.extend({
 			});
 		},
 
+		addVisibleUser() {
+			(this as any).apis.input({
+				title: 'ユーザー名を入力してください'
+			}).then(username => {
+				(this as any).api('users/show', {
+					username
+				}).then(user => {
+					this.visibleUsers.push(user);
+				});
+			});
+		},
+
+		removeVisibleUser(user) {
+			this.visibleUsers = this.visibleUsers.filter(u => u != user);
+		},
+
 		post() {
 			this.posting = true;
 
@@ -270,6 +291,7 @@ export default Vue.extend({
 				poll: this.poll ? (this.$refs.poll as any).get() : undefined,
 				cw: this.useCw ? this.cw || '' : undefined,
 				visibility: this.visibility,
+				visibleUserIds: this.visibleUsers.map(u => u.id),
 				geo: this.geo ? {
 					coordinates: [this.geo.longitude, this.geo.latitude],
 					altitude: this.geo.altitude,
@@ -395,6 +417,14 @@ root(isDark)
 				border-bottom solid 1px rgba($theme-color, 0.1) !important
 				border-radius 4px 4px 0 0
 
+		> .visibleUsers
+			margin-bottom 8px
+			font-size 14px
+
+			> span
+				margin-right 16px
+				color isDark ? #fff : #666
+
 		> .medias
 			margin 0
 			padding 0
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index e8070595c..2b9fb0b1d 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -76,7 +76,11 @@ export default async (user: IUser, data: {
 		geo: data.geo || null,
 		appId: data.app ? data.app._id : null,
 		visibility: data.visibility,
-		visibleUserIds: data.visibleUsers ? data.visibleUsers.map(u => u._id) : [],
+		visibleUserIds: data.visibility == 'specified'
+			? data.visibleUsers
+				? data.visibleUsers.map(u => u._id)
+				: []
+			: [],
 
 		// 以下非正規化データ
 		_reply: data.reply ? { userId: data.reply.userId } : null,

From e9940c92214f4b70b6f8dfdf1f4dd7ce1814e67d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 29 Apr 2018 07:01:47 +0900
Subject: [PATCH 08/12] wip

---
 .../desktop/views/components/notes.note.vue   |  1 +
 .../desktop/views/components/post-form.vue    |  2 +-
 .../views/components/sub-note-content.vue     |  1 +
 .../app/mobile/views/components/note.vue      |  5 +-
 .../views/components/sub-note-content.vue     |  1 +
 src/models/note.ts                            | 19 ++---
 src/services/note/create.ts                   | 77 +++++++++++--------
 7 files changed, 63 insertions(+), 43 deletions(-)

diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 217587ec6..e8fabe3c6 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -40,6 +40,7 @@
 				</p>
 				<div class="content" v-show="p.cw == null || showContent">
 					<div class="text">
+						<span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
 						<a class="reply" v-if="p.reply">%fa:reply%</a>
 						<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
 						<a class="rp" v-if="p.renote">RP:</a>
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index a9ce1354d..d704cdc3c 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -291,7 +291,7 @@ export default Vue.extend({
 				poll: this.poll ? (this.$refs.poll as any).get() : undefined,
 				cw: this.useCw ? this.cw || '' : undefined,
 				visibility: this.visibility,
-				visibleUserIds: this.visibleUsers.map(u => u.id),
+				visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
 				geo: this.geo ? {
 					coordinates: [this.geo.longitude, this.geo.latitude],
 					altitude: this.geo.altitude,
diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue
index 51ee93cba..dd4012039 100644
--- a/src/client/app/desktop/views/components/sub-note-content.vue
+++ b/src/client/app/desktop/views/components/sub-note-content.vue
@@ -1,6 +1,7 @@
 <template>
 <div class="mk-sub-note-content">
 	<div class="body">
+		<span v-if="note.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
 		<a class="reply" v-if="note.replyId">%fa:reply%</a>
 		<mk-note-html :text="note.text" :i="os.i"/>
 		<a class="rp" v-if="note.renoteId" :href="`/note:${note.renoteId}`">RP: ...</a>
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index 3eca49a43..5202e0eb5 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -37,9 +37,8 @@
 				</p>
 				<div class="content" v-show="p.cw == null || showContent">
 					<div class="text">
-						<a class="reply" v-if="p.reply">
-							%fa:reply%
-						</a>
+						<span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
+						<a class="reply" v-if="p.reply">%fa:reply%</a>
 						<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
 						<a class="rp" v-if="p.renote != null">RP:</a>
 					</div>
diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue
index 54cc74f7f..cc50977a5 100644
--- a/src/client/app/mobile/views/components/sub-note-content.vue
+++ b/src/client/app/mobile/views/components/sub-note-content.vue
@@ -1,6 +1,7 @@
 <template>
 <div class="mk-sub-note-content">
 	<div class="body">
+		<span v-if="note.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
 		<a class="reply" v-if="note.replyId">%fa:reply%</a>
 		<mk-note-html v-if="note.text" :text="note.text" :i="os.i"/>
 		<a class="rp" v-if="note.renoteId">RP: ...</a>
diff --git a/src/models/note.ts b/src/models/note.ts
index 918ef6d69..3256a8c15 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -163,9 +163,9 @@ export const pack = async (
 		detail: boolean
 	}
 ) => {
-	const opts = options || {
-		detail: true,
-	};
+	const opts = Object.assign({
+		detail: true
+	}, options);
 
 	// Me
 	const meId: mongo.ObjectID = me
@@ -208,7 +208,7 @@ export const pack = async (
 			hide = false;
 		} else {
 			// 指定されているかどうか
-			const specified = _note.visibleUserIds.test(id => id.equals(meId));
+			const specified = _note.visibleUserIds.some(id => id.equals(meId));
 
 			if (specified) {
 				hide = false;
@@ -245,6 +245,9 @@ export const pack = async (
 	_note.id = _note._id;
 	delete _note._id;
 
+	delete _note._user;
+	delete _note._reply;
+	delete _note.repost;
 	delete _note.mentions;
 	if (_note.geo) delete _note.geo.type;
 
@@ -262,11 +265,9 @@ export const pack = async (
 	}
 
 	// Populate media
-	if (_note.mediaIds && !hide) {
-		_note.media = Promise.all(_note.mediaIds.map(fileId =>
-			packFile(fileId)
-		));
-	}
+	_note.media = hide ? [] : Promise.all(_note.mediaIds.map(fileId =>
+		packFile(fileId)
+	));
 
 	// When requested a detailed note data
 	if (opts.detail) {
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 2b9fb0b1d..8d6626713 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -123,42 +123,59 @@ export default async (user: IUser, data: {
 	if (note.channelId == null) {
 		if (!silent) {
 			if (isLocalUser(user)) {
-				// Publish event to myself's stream
-				stream(note.userId, 'note', noteObj);
+				if (note.visibility == 'private') {
+					// Publish event to myself's stream
+					stream(note.userId, 'note', await pack(note, user, {
+						detail: true
+					}));
+				} else {
+					// Publish event to myself's stream
+					stream(note.userId, 'note', noteObj);
 
-				// Publish note to local timeline stream
-				publishLocalTimelineStream(noteObj);
+					// Publish note to local timeline stream
+					publishLocalTimelineStream(noteObj);
+				}
 			}
 
 			// Publish note to global timeline stream
 			publishGlobalTimelineStream(noteObj);
 
-			// フォロワーに配信
-			Following.find({
-				followeeId: note.userId
-			}).then(followers => {
-				followers.map(async following => {
-					const follower = following._follower;
-
-					if (isLocalUser(follower)) {
-						// ストーキングしていない場合
-						if (!following.stalk) {
-							// この投稿が返信ならスキップ
-							if (note.replyId && !note._reply.userId.equals(following.followerId) && !note._reply.userId.equals(note.userId)) return;
-						}
-
-						// Publish event to followers stream
-						stream(following.followerId, 'note', noteObj);
-					} else {
-						//#region AP配送
-						// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
-						if (isLocalUser(user)) {
-							deliver(user, await render(), follower.inbox);
-						}
-						//#endergion
-					}
+			if (note.visibility == 'specified') {
+				data.visibleUsers.forEach(async u => {
+					stream(u._id, 'note', await pack(note, u, {
+						detail: true
+					}));
 				});
-			});
+			}
+
+			if (note.visibility == 'public' || note.visibility == 'home' || note.visibility == 'followers') {
+				// フォロワーに配信
+				Following.find({
+					followeeId: note.userId
+				}).then(followers => {
+					followers.map(async following => {
+						const follower = following._follower;
+
+						if (isLocalUser(follower)) {
+							// ストーキングしていない場合
+							if (!following.stalk) {
+								// この投稿が返信ならスキップ
+								if (note.replyId && !note._reply.userId.equals(following.followerId) && !note._reply.userId.equals(note.userId)) return;
+							}
+
+							// Publish event to followers stream
+							stream(following.followerId, 'note', noteObj);
+						} else {
+							//#region AP配送
+							// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
+							if (isLocalUser(user)) {
+								deliver(user, await render(), follower.inbox);
+							}
+							//#endergion
+						}
+					});
+				});
+			}
 
 			// リストに配信
 			UserList.find({
@@ -170,7 +187,7 @@ export default async (user: IUser, data: {
 			});
 		}
 
-		//#region AP配送
+		//#region リプライとAnnounceのAP配送
 		const render = async () => {
 			const content = data.renote && data.text == null
 				? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote))

From f2874d778a089c9825360857a4f2363f4aff2e45 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 29 Apr 2018 08:51:17 +0900
Subject: [PATCH 09/12] wip

---
 src/client/app/auth/views/form.vue            |   4 +-
 src/client/app/auth/views/index.vue           |   2 +-
 .../common/views/components/autocomplete.vue  |  12 +-
 .../components/messaging-room.message.vue     |   8 +-
 .../views/components/messaging-room.vue       |  16 +--
 .../app/common/views/components/messaging.vue |  18 +--
 .../app/common/views/components/note-menu.vue |   2 +-
 .../app/common/views/components/poll.vue      |   4 +-
 .../app/common/views/components/signin.vue    |   4 +-
 .../app/common/views/components/signup.vue    |   4 +-
 .../views/components/stream-indicator.vue     |   2 +-
 .../views/components/visibility-chooser.vue   |  18 ++-
 .../views/components/welcome-timeline.vue     |   2 +-
 .../app/common/views/widgets/access-log.vue   |   2 +-
 .../app/common/views/widgets/calendar.vue     |   4 +-
 .../app/common/views/widgets/donation.vue     |   2 +-
 src/client/app/common/views/widgets/rss.vue   |   2 +-
 .../app/common/views/widgets/server.pie.vue   |   4 +-
 .../app/common/views/widgets/slideshow.vue    |   4 +-
 .../views/components/activity.calendar.vue    |   2 +-
 .../app/desktop/views/components/calendar.vue |  12 +-
 .../views/components/context-menu.menu.vue    |   2 +-
 .../desktop/views/components/context-menu.vue |   2 +-
 .../app/desktop/views/components/dialog.vue   |   2 +-
 .../desktop/views/components/drive.file.vue   |   4 +-
 .../app/desktop/views/components/drive.vue    |   4 +-
 .../views/components/ellipsis-icon.vue        |   2 +-
 .../app/desktop/views/components/home.vue     |   2 +-
 .../views/components/media-image-dialog.vue   |   2 +-
 .../views/components/media-video-dialog.vue   |   2 +-
 .../app/desktop/views/components/mentions.vue |   2 +-
 .../views/components/note-detail.sub.vue      |   1 +
 .../desktop/views/components/note-detail.vue  |   3 +-
 .../desktop/views/components/notes.note.vue   |   2 +-
 .../views/components/notifications.vue        |  16 +--
 .../desktop/views/components/post-form.vue    |   3 +-
 .../app/desktop/views/components/timeline.vue |   2 +-
 .../views/components/ui.header.account.vue    |   4 +-
 .../components/ui.header.notifications.vue    |   4 +-
 .../views/components/ui.header.search.vue     |   4 +-
 .../desktop/views/components/ui.header.vue    |   2 +-
 .../desktop/views/components/user-preview.vue |   2 +-
 .../desktop/views/components/users-list.vue   |   2 +-
 .../views/components/widget-container.vue     |   4 +-
 .../app/desktop/views/components/window.vue   |  26 ++--
 src/client/app/desktop/views/pages/search.vue |   2 +-
 .../pages/user/user.followers-you-know.vue    |   4 +-
 .../desktop/views/pages/user/user.friends.vue |   4 +-
 .../desktop/views/pages/user/user.header.vue  |   6 +-
 .../desktop/views/pages/user/user.home.vue    |   4 +-
 .../desktop/views/pages/user/user.photos.vue  |   4 +-
 .../desktop/views/pages/user/user.profile.vue |   2 +-
 .../app/desktop/views/pages/welcome.vue       |   4 +-
 .../app/desktop/views/widgets/channel.vue     |   4 +-
 .../app/desktop/views/widgets/post-form.vue   |   4 +-
 .../app/desktop/views/widgets/profile.vue     |   8 +-
 .../views/components/drive-file-chooser.vue   |   2 +-
 .../views/components/drive-folder-chooser.vue |   2 +-
 .../views/components/drive.file-detail.vue    |   4 +-
 .../app/mobile/views/components/drive.vue     |   6 +-
 .../mobile/views/components/friends-maker.vue |   2 +-
 .../mobile/views/components/note-detail.vue   |   1 +
 .../mobile/views/components/notifications.vue |   7 +-
 .../app/mobile/views/components/post-form.vue | 114 +++++++++++++-----
 .../mobile/views/components/users-list.vue    |   8 +-
 .../views/components/widget-container.vue     |   2 +-
 src/client/app/mobile/views/pages/home.vue    |   2 +-
 .../mobile/views/pages/profile-setting.vue    |   4 +-
 src/client/app/mobile/views/pages/search.vue  |   2 +-
 .../app/mobile/views/pages/selectdrive.vue    |   2 +-
 .../app/mobile/views/pages/settings.vue       |   4 +-
 src/client/app/mobile/views/pages/signup.vue  |   2 +-
 src/client/app/mobile/views/pages/welcome.vue |   8 +-
 .../app/mobile/views/widgets/profile.vue      |   6 +-
 src/renderers/get-note-summary.ts             |   4 +
 75 files changed, 265 insertions(+), 190 deletions(-)

diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue
index b323907eb..152b90042 100644
--- a/src/client/app/auth/views/form.vue
+++ b/src/client/app/auth/views/form.vue
@@ -94,13 +94,13 @@ export default Vue.extend({
 			margin 0 auto -38px auto
 			border solid 5px #fff
 			border-radius 100%
-			box-shadow 0 2px 2px rgba(0, 0, 0, 0.1)
+			box-shadow 0 2px 2px rgba(#000, 0.1)
 
 	> .app
 		padding 44px 16px 0 16px
 		color #555
 		background #eee
-		box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) inset
+		box-shadow 0 2px 2px rgba(#000, 0.1) inset
 
 		&:after
 			content ''
diff --git a/src/client/app/auth/views/index.vue b/src/client/app/auth/views/index.vue
index e1e1b265e..0fcd9bfe5 100644
--- a/src/client/app/auth/views/index.vue
+++ b/src/client/app/auth/views/index.vue
@@ -94,7 +94,7 @@ export default Vue.extend({
 		margin 0 auto
 		text-align center
 		background #fff
-		box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
+		box-shadow 0px 4px 16px rgba(#000, 0.2)
 
 		> .fetching
 			margin 0
diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index 5c8f61a2a..84173d20b 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -234,7 +234,7 @@ export default Vue.extend({
 	margin-top calc(1em + 8px)
 	overflow hidden
 	background #fff
-	border solid 1px rgba(0, 0, 0, 0.1)
+	border solid 1px rgba(#000, 0.1)
 	border-radius 4px
 	transition top 0.1s ease, left 0.1s ease
 
@@ -253,7 +253,7 @@ export default Vue.extend({
 			white-space nowrap
 			overflow hidden
 			font-size 0.9em
-			color rgba(0, 0, 0, 0.8)
+			color rgba(#000, 0.8)
 			cursor default
 
 			&, *
@@ -285,10 +285,10 @@ export default Vue.extend({
 
 		.name
 			margin 0 8px 0 0
-			color rgba(0, 0, 0, 0.8)
+			color rgba(#000, 0.8)
 
 		.username
-			color rgba(0, 0, 0, 0.3)
+			color rgba(#000, 0.3)
 
 	> .emojis > li
 
@@ -298,10 +298,10 @@ export default Vue.extend({
 			width 24px
 
 		.name
-			color rgba(0, 0, 0, 0.8)
+			color rgba(#000, 0.8)
 
 		.alias
 			margin 0 0 0 8px
-			color rgba(0, 0, 0, 0.3)
+			color rgba(#000, 0.3)
 
 </style>
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index afd700e77..70df899f5 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -134,7 +134,7 @@ export default Vue.extend({
 				bottom -4px
 				left -12px
 				margin 0
-				color rgba(0, 0, 0, 0.5)
+				color rgba(#000, 0.5)
 				font-size 11px
 
 			> .content
@@ -146,7 +146,7 @@ export default Vue.extend({
 					overflow hidden
 					overflow-wrap break-word
 					font-size 1em
-					color rgba(0, 0, 0, 0.5)
+					color rgba(#000, 0.5)
 
 				> .text
 					display block
@@ -155,7 +155,7 @@ export default Vue.extend({
 					overflow hidden
 					overflow-wrap break-word
 					font-size 1em
-					color rgba(0, 0, 0, 0.8)
+					color rgba(#000, 0.8)
 
 					& + .file
 						> a
@@ -195,7 +195,7 @@ export default Vue.extend({
 			display block
 			margin 2px 0 0 0
 			font-size 10px
-			color rgba(0, 0, 0, 0.4)
+			color rgba(#000, 0.4)
 
 			> [data-fa]
 				margin-left 4px
diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue
index 38202d758..a45114e6b 100644
--- a/src/client/app/common/views/components/messaging-room.vue
+++ b/src/client/app/common/views/components/messaging-room.vue
@@ -256,7 +256,7 @@ export default Vue.extend({
 			padding 16px 8px 8px 8px
 			text-align center
 			font-size 0.8em
-			color rgba(0, 0, 0, 0.4)
+			color rgba(#000, 0.4)
 
 			[data-fa]
 				margin-right 4px
@@ -267,7 +267,7 @@ export default Vue.extend({
 			padding 16px 8px 8px 8px
 			text-align center
 			font-size 0.8em
-			color rgba(0, 0, 0, 0.4)
+			color rgba(#000, 0.4)
 
 			[data-fa]
 				margin-right 4px
@@ -278,7 +278,7 @@ export default Vue.extend({
 			padding 16px
 			text-align center
 			font-size 0.8em
-			color rgba(0, 0, 0, 0.4)
+			color rgba(#000, 0.4)
 
 			[data-fa]
 				margin-right 4px
@@ -289,14 +289,14 @@ export default Vue.extend({
 			padding 0 12px
 			line-height 24px
 			color #fff
-			background rgba(0, 0, 0, 0.3)
+			background rgba(#000, 0.3)
 			border-radius 12px
 
 			&:hover
-				background rgba(0, 0, 0, 0.4)
+				background rgba(#000, 0.4)
 
 			&:active
-				background rgba(0, 0, 0, 0.5)
+				background rgba(#000, 0.5)
 
 			&.fetching
 				cursor wait
@@ -322,7 +322,7 @@ export default Vue.extend({
 				left 0
 				right 0
 				margin 0 auto
-				background rgba(0, 0, 0, 0.1)
+				background rgba(#000, 0.1)
 
 			> span
 				display inline-block
@@ -330,7 +330,7 @@ export default Vue.extend({
 				padding 0 16px
 				//font-weight bold
 				line-height 32px
-				color rgba(0, 0, 0, 0.3)
+				color rgba(#000, 0.3)
 				background #fff
 
 	> footer
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index 6f799b34b..6f8fcb3a7 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -205,7 +205,7 @@ root(isDark)
 		z-index 1
 		width 100%
 		background #fff
-		box-shadow 0 0px 2px rgba(0, 0, 0, 0.2)
+		box-shadow 0 0px 2px rgba(#000, 0.2)
 
 		> .form
 			padding 8px
@@ -279,7 +279,7 @@ root(isDark)
 					vertical-align top
 					white-space nowrap
 					overflow hidden
-					color rgba(0, 0, 0, 0.8)
+					color rgba(#000, 0.8)
 					text-decoration none
 					transition none
 					cursor pointer
@@ -318,11 +318,11 @@ root(isDark)
 						margin 0 8px 0 0
 						/*font-weight bold*/
 						font-weight normal
-						color rgba(0, 0, 0, 0.8)
+						color rgba(#000, 0.8)
 
 					.username
 						font-weight normal
-						color rgba(0, 0, 0, 0.3)
+						color rgba(#000, 0.3)
 
 	> .history
 
@@ -383,17 +383,17 @@ root(isDark)
 						overflow hidden
 						text-overflow ellipsis
 						font-size 1em
-						color isDark ? #fff : rgba(0, 0, 0, 0.9)
+						color isDark ? #fff : rgba(#000, 0.9)
 						font-weight bold
 						transition all 0.1s ease
 
 					> .username
 						margin 0 8px
-						color isDark ? #606984 : rgba(0, 0, 0, 0.5)
+						color isDark ? #606984 : rgba(#000, 0.5)
 
 					> .mk-time
 						margin 0 0 0 auto
-						color isDark ? #606984 : rgba(0, 0, 0, 0.5)
+						color isDark ? #606984 : rgba(#000, 0.5)
 						font-size 80%
 
 				> .avatar
@@ -413,10 +413,10 @@ root(isDark)
 						overflow hidden
 						overflow-wrap break-word
 						font-size 1.1em
-						color isDark ? #fff : rgba(0, 0, 0, 0.8)
+						color isDark ? #fff : rgba(#000, 0.8)
 
 						.me
-							color isDark ? rgba(#fff, 0.7) : rgba(0, 0, 0, 0.4)
+							color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.4)
 
 					> .image
 						display block
diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/app/common/views/components/note-menu.vue
index 3e4be425d..88dc22aaf 100644
--- a/src/client/app/common/views/components/note-menu.vue
+++ b/src/client/app/common/views/components/note-menu.vue
@@ -105,7 +105,7 @@ $border-color = rgba(27, 31, 35, 0.15)
 		z-index 10000
 		width 100%
 		height 100%
-		background rgba(0, 0, 0, 0.1)
+		background rgba(#000, 0.1)
 		opacity 0
 
 	> .popover
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
index de4373f56..46e41cbcd 100644
--- a/src/client/app/common/views/components/poll.vue
+++ b/src/client/app/common/views/components/poll.vue
@@ -88,10 +88,10 @@ root(isDark)
 			cursor pointer
 
 			&:hover
-				background rgba(0, 0, 0, 0.05)
+				background rgba(#000, 0.05)
 
 			&:active
-				background rgba(0, 0, 0, 0.1)
+				background rgba(#000, 0.1)
 
 			> .backdrop
 				position absolute
diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue
index 25f90a2f1..7fb9fc3fd 100644
--- a/src/client/app/common/views/components/signin.vue
+++ b/src/client/app/common/views/components/signin.vue
@@ -91,7 +91,7 @@ export default Vue.extend({
 			width 100%
 			line-height 44px
 			font-size 1em
-			color rgba(0, 0, 0, 0.7)
+			color rgba(#000, 0.7)
 			background #fff
 			outline none
 			border solid 1px #eee
@@ -117,7 +117,7 @@ export default Vue.extend({
 		margin -6px 0 0 0
 		width 100%
 		font-size 1.2em
-		color rgba(0, 0, 0, 0.5)
+		color rgba(#000, 0.5)
 		outline none
 		border none
 		border-radius 0
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index 33a559ff8..516979acd 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -234,13 +234,13 @@ export default Vue.extend({
 		color #333 !important
 		background #fff !important
 		outline none
-		border solid 1px rgba(0, 0, 0, 0.1)
+		border solid 1px rgba(#000, 0.1)
 		border-radius 4px
 		box-shadow 0 0 0 114514px #fff inset
 		transition all .3s ease
 
 		&:hover
-			border-color rgba(0, 0, 0, 0.2)
+			border-color rgba(#000, 0.2)
 			transition all .1s ease
 
 		&:focus
diff --git a/src/client/app/common/views/components/stream-indicator.vue b/src/client/app/common/views/components/stream-indicator.vue
index 93758102d..d573db32e 100644
--- a/src/client/app/common/views/components/stream-indicator.vue
+++ b/src/client/app/common/views/components/stream-indicator.vue
@@ -73,7 +73,7 @@ export default Vue.extend({
 	padding 6px 12px
 	font-size 0.9em
 	color #fff
-	background rgba(0, 0, 0, 0.8)
+	background rgba(#000, 0.8)
 	border-radius 4px
 
 	> p
diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
index dd36d32e7..50f0877ae 100644
--- a/src/client/app/common/views/components/visibility-chooser.vue
+++ b/src/client/app/common/views/components/visibility-chooser.vue
@@ -53,18 +53,28 @@ export default Vue.extend({
 			const width = popover.offsetWidth;
 			const height = popover.offsetHeight;
 
+			let left;
+			let top;
+
 			if (this.compact) {
 				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
 				const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
-				popover.style.left = (x - (width / 2)) + 'px';
-				popover.style.top = (y - (height / 2)) + 'px';
+				left = (x - (width / 2));
+				top = (y - (height / 2));
 			} else {
 				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
 				const y = rect.top + window.pageYOffset + this.source.offsetHeight;
-				popover.style.left = (x - (width / 2)) + 'px';
-				popover.style.top = y + 'px';
+				left = (x - (width / 2));
+				top = y;
 			}
 
+			if (left + width > window.innerWidth) {
+				left = window.innerWidth - width;
+			}
+
+			popover.style.left = left + 'px';
+			popover.style.top = top + 'px';
+
 			anime({
 				targets: this.$refs.backdrop,
 				opacity: 1,
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index a80bc04f7..349797690 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -62,7 +62,7 @@ export default Vue.extend({
 		overflow-wrap break-word
 		font-size .9em
 		color #4C4C4C
-		border-bottom 1px solid rgba(0, 0, 0, 0.05)
+		border-bottom 1px solid rgba(#000, 0.05)
 
 		&:after
 			content ""
diff --git a/src/client/app/common/views/widgets/access-log.vue b/src/client/app/common/views/widgets/access-log.vue
index 637ba328c..0b1c7fe2d 100644
--- a/src/client/app/common/views/widgets/access-log.vue
+++ b/src/client/app/common/views/widgets/access-log.vue
@@ -78,7 +78,7 @@ export default define({
 		color #555
 
 		&:nth-child(odd)
-			background rgba(0, 0, 0, 0.025)
+			background rgba(#000, 0.025)
 
 		> b
 			margin-right 4px
diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue
index b3b5b3583..0bb503759 100644
--- a/src/client/app/common/views/widgets/calendar.vue
+++ b/src/client/app/common/views/widgets/calendar.vue
@@ -113,7 +113,7 @@ root(isDark)
 	padding 16px 0
 	color isDark ? #c5ced6 :#777
 	background isDark ? #282C37 : #fff
-	border solid 1px rgba(0, 0, 0, 0.075)
+	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 
 	&[data-special='on-new-years-day']
@@ -126,7 +126,7 @@ root(isDark)
 	&[data-mobile]
 		border none
 		border-radius 8px
-		box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+		box-shadow 0 0 0 1px rgba(#000, 0.2)
 
 	&:after
 		content ""
diff --git a/src/client/app/common/views/widgets/donation.vue b/src/client/app/common/views/widgets/donation.vue
index 1063c2c27..e35462611 100644
--- a/src/client/app/common/views/widgets/donation.vue
+++ b/src/client/app/common/views/widgets/donation.vue
@@ -46,7 +46,7 @@ root(isDark)
 		border none
 		background #ead8bb
 		border-radius 8px
-		box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+		box-shadow 0 0 0 1px rgba(#000, 0.2)
 
 		> article
 			> h1
diff --git a/src/client/app/common/views/widgets/rss.vue b/src/client/app/common/views/widgets/rss.vue
index 1a6336252..f0ba11678 100644
--- a/src/client/app/common/views/widgets/rss.vue
+++ b/src/client/app/common/views/widgets/rss.vue
@@ -92,7 +92,7 @@ root(isDark)
 					padding 8px 16px
 
 					&:nth-child(even)
-						background rgba(0, 0, 0, 0.05)
+						background rgba(#000, 0.05)
 
 .mkw-rss[data-darkmode]
 	root(true)
diff --git a/src/client/app/common/views/widgets/server.pie.vue b/src/client/app/common/views/widgets/server.pie.vue
index 16859fe07..532041ae7 100644
--- a/src/client/app/common/views/widgets/server.pie.vue
+++ b/src/client/app/common/views/widgets/server.pie.vue
@@ -5,7 +5,7 @@
 		cx="50%" cy="50%"
 		fill="none"
 		stroke-width="0.1"
-		stroke="rgba(0, 0, 0, 0.05)"/>
+		stroke="rgba(#000, 0.05)"/>
 	<circle
 		:r="r"
 		cx="50%" cy="50%"
@@ -56,7 +56,7 @@ root(isDark)
 
 	> text
 		font-size 0.15px
-		fill isDark ? rgba(#fff, 0.6) : rgba(0, 0, 0, 0.6)
+		fill isDark ? rgba(#fff, 0.6) : rgba(#000, 0.6)
 
 svg[data-darkmode]
 	root(true)
diff --git a/src/client/app/common/views/widgets/slideshow.vue b/src/client/app/common/views/widgets/slideshow.vue
index ad32299f3..95be4b94f 100644
--- a/src/client/app/common/views/widgets/slideshow.vue
+++ b/src/client/app/common/views/widgets/slideshow.vue
@@ -122,13 +122,13 @@ export default define({
 .mkw-slideshow
 	overflow hidden
 	background #fff
-	border solid 1px rgba(0, 0, 0, 0.075)
+	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 
 	&[data-mobile]
 		border none
 		border-radius 8px
-		box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+		box-shadow 0 0 0 1px rgba(#000, 0.2)
 
 	> div
 		width 100%
diff --git a/src/client/app/desktop/views/components/activity.calendar.vue b/src/client/app/desktop/views/components/activity.calendar.vue
index 8b43536c2..e48857107 100644
--- a/src/client/app/desktop/views/components/activity.calendar.vue
+++ b/src/client/app/desktop/views/components/activity.calendar.vue
@@ -61,6 +61,6 @@ svg
 
 		&.day
 			&:hover
-				fill rgba(0, 0, 0, 0.05)
+				fill rgba(#000, 0.05)
 
 </style>
diff --git a/src/client/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue
index 483a4c718..1d8cc4f3a 100644
--- a/src/client/app/desktop/views/components/calendar.vue
+++ b/src/client/app/desktop/views/components/calendar.vue
@@ -136,7 +136,7 @@ export default Vue.extend({
 root(isDark)
 	color isDark ? #c5ced6 : #777
 	background isDark ? #282C37 : #fff
-	border solid 1px rgba(0, 0, 0, 0.075)
+	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 
 	&[data-melt]
@@ -152,7 +152,7 @@ root(isDark)
 		font-size 0.9em
 		font-weight bold
 		color #888
-		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+		box-shadow 0 1px rgba(#000, 0.07)
 
 		> [data-fa]
 			margin-right 4px
@@ -214,10 +214,10 @@ root(isDark)
 					border-radius 6px
 
 				&:hover > div
-					background rgba(0, 0, 0, 0.025)
+					background rgba(#000, 0.025)
 
 				&:active > div
-					background rgba(0, 0, 0, 0.05)
+					background rgba(#000, 0.05)
 
 				&[data-is-donichi]
 					color #ef95a0
@@ -233,10 +233,10 @@ root(isDark)
 					font-weight bold
 
 					> div
-						background rgba(0, 0, 0, 0.025)
+						background rgba(#000, 0.025)
 
 					&:active > div
-						background rgba(0, 0, 0, 0.05)
+						background rgba(#000, 0.05)
 
 				&[data-today]
 					> div
diff --git a/src/client/app/desktop/views/components/context-menu.menu.vue b/src/client/app/desktop/views/components/context-menu.menu.vue
index 3d884e0b8..843604a05 100644
--- a/src/client/app/desktop/views/components/context-menu.menu.vue
+++ b/src/client/app/desktop/views/components/context-menu.menu.vue
@@ -106,7 +106,7 @@ root(isDark)
 		width $width
 		background isDark ? #282c37 :#fff
 		border-radius 0 4px 4px 4px
-		box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2)
+		box-shadow 2px 2px 8px rgba(#000, 0.2)
 		transition visibility 0s linear 0.2s
 
 .menu[data-darkmode]
diff --git a/src/client/app/desktop/views/components/context-menu.vue b/src/client/app/desktop/views/components/context-menu.vue
index d7548c441..60a33f9c9 100644
--- a/src/client/app/desktop/views/components/context-menu.vue
+++ b/src/client/app/desktop/views/components/context-menu.vue
@@ -68,7 +68,7 @@ root(isDark)
 	font-size 0.8em
 	background isDark ? #282c37 : #fff
 	border-radius 0 4px 4px 4px
-	box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2)
+	box-shadow 2px 2px 8px rgba(#000, 0.2)
 	opacity 0
 
 .context-menu[data-darkmode]
diff --git a/src/client/app/desktop/views/components/dialog.vue b/src/client/app/desktop/views/components/dialog.vue
index fa17e4a9d..aff21c175 100644
--- a/src/client/app/desktop/views/components/dialog.vue
+++ b/src/client/app/desktop/views/components/dialog.vue
@@ -102,7 +102,7 @@ export default Vue.extend({
 		left 0
 		width 100%
 		height 100%
-		background rgba(0, 0, 0, 0.7)
+		background rgba(#000, 0.7)
 		opacity 0
 		pointer-events none
 
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
index a683af68a..39881711f 100644
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ b/src/client/app/desktop/views/components/drive.file.vue
@@ -195,7 +195,7 @@ root(isDark)
 		cursor pointer
 
 	&:hover
-		background rgba(0, 0, 0, 0.05)
+		background rgba(#000, 0.05)
 
 		> .label
 			&:before
@@ -203,7 +203,7 @@ root(isDark)
 				background #0b65a5
 
 	&:active
-		background rgba(0, 0, 0, 0.1)
+		background rgba(#000, 0.1)
 
 		> .label
 			&:before
diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue
index aebface11..973df1014 100644
--- a/src/client/app/desktop/views/components/drive.vue
+++ b/src/client/app/desktop/views/components/drive.vue
@@ -587,7 +587,7 @@ root(isDark)
 		font-size 0.9em
 		color isDark ? #d2d9dc : #555
 		background isDark ? #282c37 : #fff
-		box-shadow 0 1px 0 rgba(0, 0, 0, 0.05)
+		box-shadow 0 1px 0 rgba(#000, 0.05)
 
 		&, *
 			user-select none
@@ -733,7 +733,7 @@ root(isDark)
 				display inline-block
 				position absolute
 				top 0
-				background-color rgba(0, 0, 0, 0.3)
+				background-color rgba(#000, 0.3)
 				border-radius 100%
 
 				animation sk-bounce 2.0s infinite ease-in-out
diff --git a/src/client/app/desktop/views/components/ellipsis-icon.vue b/src/client/app/desktop/views/components/ellipsis-icon.vue
index c54a7db29..4a5a0f23d 100644
--- a/src/client/app/desktop/views/components/ellipsis-icon.vue
+++ b/src/client/app/desktop/views/components/ellipsis-icon.vue
@@ -14,7 +14,7 @@
 		display inline-block
 		width 18px
 		height 18px
-		background-color rgba(0, 0, 0, 0.3)
+		background-color rgba(#000, 0.3)
 		border-radius 100%
 		animation bounce 1.4s infinite ease-in-out both
 
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index 5c5b46217..4343a7fb7 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -251,7 +251,7 @@ root(isDark)
 		height 48px
 		color isDark ? #fff : #000
 		background isDark ? #313543 : #f7f7f7
-		box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
+		box-shadow 0 1px 1px rgba(#000, 0.075)
 
 		> a
 			display block
diff --git a/src/client/app/desktop/views/components/media-image-dialog.vue b/src/client/app/desktop/views/components/media-image-dialog.vue
index dec140d1c..026522d90 100644
--- a/src/client/app/desktop/views/components/media-image-dialog.vue
+++ b/src/client/app/desktop/views/components/media-image-dialog.vue
@@ -52,7 +52,7 @@ export default Vue.extend({
 		left 0
 		width 100%
 		height 100%
-		background rgba(0, 0, 0, 0.7)
+		background rgba(#000, 0.7)
 
 	> img
 		position fixed
diff --git a/src/client/app/desktop/views/components/media-video-dialog.vue b/src/client/app/desktop/views/components/media-video-dialog.vue
index cbf862cd1..959cefa42 100644
--- a/src/client/app/desktop/views/components/media-video-dialog.vue
+++ b/src/client/app/desktop/views/components/media-video-dialog.vue
@@ -54,7 +54,7 @@ export default Vue.extend({
 		left 0
 		width 100%
 		height 100%
-		background rgba(0, 0, 0, 0.7)
+		background rgba(#000, 0.7)
 
 	> video
 		position fixed
diff --git a/src/client/app/desktop/views/components/mentions.vue b/src/client/app/desktop/views/components/mentions.vue
index 53d08a0ec..66bdab5c0 100644
--- a/src/client/app/desktop/views/components/mentions.vue
+++ b/src/client/app/desktop/views/components/mentions.vue
@@ -85,7 +85,7 @@ export default Vue.extend({
 
 .mk-mentions
 	background #fff
-	border solid 1px rgba(0, 0, 0, 0.075)
+	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 
 	> header
diff --git a/src/client/app/desktop/views/components/note-detail.sub.vue b/src/client/app/desktop/views/components/note-detail.sub.vue
index fb3342130..5175c8bd4 100644
--- a/src/client/app/desktop/views/components/note-detail.sub.vue
+++ b/src/client/app/desktop/views/components/note-detail.sub.vue
@@ -17,6 +17,7 @@
 		</header>
 		<div class="body">
 			<div class="text">
+				<span v-if="note.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
 				<mk-note-html v-if="note.text" :text="note.text" :i="os.i"/>
 			</div>
 			<div class="media" v-if="note.mediaIds.length > 0">
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index eb12ea109..525023349 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -39,6 +39,7 @@
 		</header>
 		<div class="body">
 			<div class="text">
+				<span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
 				<mk-note-html v-if="p.text" :text="p.text" :i="os.i"/>
 			</div>
 			<div class="media" v-if="p.media.length > 0">
@@ -222,7 +223,7 @@ root(isDark)
 	overflow hidden
 	text-align left
 	background isDark ? #282C37 : #fff
-	border solid 1px rgba(0, 0, 0, 0.1)
+	border solid 1px rgba(#000, 0.1)
 	border-radius 8px
 
 	> .read-more
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index e8fabe3c6..4d7e6ee8b 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -607,7 +607,7 @@ root(isDark)
 
 	> .detail
 		padding-top 4px
-		background rgba(0, 0, 0, 0.0125)
+		background rgba(#000, 0.0125)
 
 .note[data-darkmode]
 	root(true)
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 47463b8b5..36e9dce6a 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -204,7 +204,7 @@ root(isDark)
 				padding 16px
 				overflow-wrap break-word
 				font-size 0.9em
-				border-bottom solid 1px isDark ? #1c2023 : rgba(0, 0, 0, 0.05)
+				border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05)
 
 				&:last-child
 					border-bottom none
@@ -215,7 +215,7 @@ root(isDark)
 					top 16px
 					right 12px
 					vertical-align top
-					color isDark ? #606984 : rgba(0, 0, 0, 0.6)
+					color isDark ? #606984 : rgba(#000, 0.6)
 					font-size small
 
 				&:after
@@ -250,10 +250,10 @@ root(isDark)
 							margin-right 4px
 
 				.note-preview
-					color isDark ? #c2cad4 : rgba(0, 0, 0, 0.7)
+					color isDark ? #c2cad4 : rgba(#000, 0.7)
 
 				.note-ref
-					color isDark ? #c2cad4 : rgba(0, 0, 0, 0.7)
+					color isDark ? #c2cad4 : rgba(#000, 0.7)
 
 					[data-fa]
 						font-size 1em
@@ -282,7 +282,7 @@ root(isDark)
 				font-size 0.8em
 				color isDark ? #666b79 : #aaa
 				background isDark ? #242731 : #fdfdfd
-				border-bottom solid 1px isDark ? #1c2023 : rgba(0, 0, 0, 0.05)
+				border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05)
 
 				span
 					margin 0 16px
@@ -295,13 +295,13 @@ root(isDark)
 		width 100%
 		padding 16px
 		color #555
-		border-top solid 1px rgba(0, 0, 0, 0.05)
+		border-top solid 1px rgba(#000, 0.05)
 
 		&:hover
-			background rgba(0, 0, 0, 0.025)
+			background rgba(#000, 0.025)
 
 		&:active
-			background rgba(0, 0, 0, 0.05)
+			background rgba(#000, 0.05)
 
 		&.fetching
 			cursor wait
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index d704cdc3c..92a7f9ada 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -364,7 +364,6 @@ root(isDark)
 		clear both
 
 	> .content
-
 		> input
 		> textarea
 			display block
@@ -585,7 +584,7 @@ root(isDark)
 			color rgba($theme-color, 0.6)
 			background isDark ? transparent : linear-gradient(to bottom, lighten($theme-color, 80%) 0%, lighten($theme-color, 90%) 100%)
 			border-color rgba($theme-color, 0.5)
-			box-shadow 0 2px 4px rgba(0, 0, 0, 0.15) inset
+			box-shadow 0 2px 4px rgba(#000, 0.15) inset
 
 		&:focus
 			&:after
diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index 2047b5dce..bcfce299f 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -59,7 +59,7 @@ export default Vue.extend({
 
 root(isDark)
 	background isDark ? #282C37 : #fff
-	border solid 1px rgba(0, 0, 0, 0.075)
+	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 
 	> header
diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue
index 5148c5b96..145666b4b 100644
--- a/src/client/app/desktop/views/components/ui.header.account.vue
+++ b/src/client/app/desktop/views/components/ui.header.account.vue
@@ -165,7 +165,7 @@ root(isDark)
 		font-size 0.8em
 		background $bgcolor
 		border-radius 4px
-		box-shadow 0 1px 4px rgba(0, 0, 0, 0.25)
+		box-shadow 0 1px 4px rgba(#000, 0.25)
 
 		&:before
 			content ""
@@ -176,7 +176,7 @@ root(isDark)
 			right 12px
 			border-top solid 14px transparent
 			border-right solid 14px transparent
-			border-bottom solid 14px rgba(0, 0, 0, 0.1)
+			border-bottom solid 14px rgba(#000, 0.1)
 			border-left solid 14px transparent
 
 		&:after
diff --git a/src/client/app/desktop/views/components/ui.header.notifications.vue b/src/client/app/desktop/views/components/ui.header.notifications.vue
index 5326f9283..ea814dd7a 100644
--- a/src/client/app/desktop/views/components/ui.header.notifications.vue
+++ b/src/client/app/desktop/views/components/ui.header.notifications.vue
@@ -125,7 +125,7 @@ root(isDark)
 		width 300px
 		background $bgcolor
 		border-radius 4px
-		box-shadow 0 1px 4px rgba(0, 0, 0, 0.25)
+		box-shadow 0 1px 4px rgba(#000, 0.25)
 
 		&:before
 			content ""
@@ -136,7 +136,7 @@ root(isDark)
 			right 74px
 			border-top solid 14px transparent
 			border-right solid 14px transparent
-			border-bottom solid 14px rgba(0, 0, 0, 0.1)
+			border-bottom solid 14px rgba(#000, 0.1)
 			border-left solid 14px transparent
 
 		&:after
diff --git a/src/client/app/desktop/views/components/ui.header.search.vue b/src/client/app/desktop/views/components/ui.header.search.vue
index 3167aab8a..1ed28ba3a 100644
--- a/src/client/app/desktop/views/components/ui.header.search.vue
+++ b/src/client/app/desktop/views/components/ui.header.search.vue
@@ -50,7 +50,7 @@ export default Vue.extend({
 		width 14em
 		height 32px
 		font-size 1em
-		background rgba(0, 0, 0, 0.05)
+		background rgba(#000, 0.05)
 		outline none
 		//border solid 1px #ddd
 		border none
@@ -62,7 +62,7 @@ export default Vue.extend({
 			color #9eaba8
 
 		&:hover
-			background rgba(0, 0, 0, 0.08)
+			background rgba(#000, 0.08)
 
 		&:focus
 			box-shadow 0 0 0 2px rgba($theme-color, 0.5) !important
diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
index 7ce85d4c3..353fd5856 100644
--- a/src/client/app/desktop/views/components/ui.header.vue
+++ b/src/client/app/desktop/views/components/ui.header.vue
@@ -103,7 +103,7 @@ root(isDark)
 	top 0
 	z-index 1000
 	width 100%
-	box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
+	box-shadow 0 1px 1px rgba(#000, 0.075)
 
 	> .main
 		height 48px
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index 2e1870800..d0111d7dc 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -94,7 +94,7 @@ root(isDark)
 	width 250px
 	background isDark ? #282c37 : #fff
 	background-clip content-box
-	border solid 1px rgba(0, 0, 0, 0.1)
+	border solid 1px rgba(#000, 0.1)
 	border-radius 4px
 	overflow hidden
 	opacity 0
diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue
index e8f4c94d4..13d0d07bb 100644
--- a/src/client/app/desktop/views/components/users-list.vue
+++ b/src/client/app/desktop/views/components/users-list.vue
@@ -119,7 +119,7 @@ export default Vue.extend({
 		overflow auto
 
 		> *
-			border-bottom solid 1px rgba(0, 0, 0, 0.05)
+			border-bottom solid 1px rgba(#000, 0.05)
 
 			> *
 				max-width 600px
diff --git a/src/client/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue
index 926d7702b..2edba5a23 100644
--- a/src/client/app/desktop/views/components/widget-container.vue
+++ b/src/client/app/desktop/views/components/widget-container.vue
@@ -36,7 +36,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 root(isDark)
 	background isDark ? #282C37 : #fff
-	border solid 1px rgba(0, 0, 0, 0.075)
+	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 	overflow hidden
 
@@ -55,7 +55,7 @@ root(isDark)
 			font-size 0.9em
 			font-weight bold
 			color isDark ? #e3e5e8 : #888
-			box-shadow 0 1px rgba(0, 0, 0, 0.07)
+			box-shadow 0 1px rgba(#000, 0.07)
 
 			> [data-fa]
 				margin-right 6px
diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
index 3de1413ac..c9820f869 100644
--- a/src/client/app/desktop/views/components/window.vue
+++ b/src/client/app/desktop/views/components/window.vue
@@ -17,14 +17,16 @@
 				<slot></slot>
 			</div>
 		</div>
-		<div class="handle top" v-if="canResize" @mousedown.prevent="onTopHandleMousedown"></div>
-		<div class="handle right" v-if="canResize" @mousedown.prevent="onRightHandleMousedown"></div>
-		<div class="handle bottom" v-if="canResize" @mousedown.prevent="onBottomHandleMousedown"></div>
-		<div class="handle left" v-if="canResize" @mousedown.prevent="onLeftHandleMousedown"></div>
-		<div class="handle top-left" v-if="canResize" @mousedown.prevent="onTopLeftHandleMousedown"></div>
-		<div class="handle top-right" v-if="canResize" @mousedown.prevent="onTopRightHandleMousedown"></div>
-		<div class="handle bottom-right" v-if="canResize" @mousedown.prevent="onBottomRightHandleMousedown"></div>
-		<div class="handle bottom-left" v-if="canResize" @mousedown.prevent="onBottomLeftHandleMousedown"></div>
+		<template v-if="canResize">
+			<div class="handle top" @mousedown.prevent="onTopHandleMousedown"></div>
+			<div class="handle right" @mousedown.prevent="onRightHandleMousedown"></div>
+			<div class="handle bottom" @mousedown.prevent="onBottomHandleMousedown"></div>
+			<div class="handle left" @mousedown.prevent="onLeftHandleMousedown"></div>
+			<div class="handle top-left" @mousedown.prevent="onTopLeftHandleMousedown"></div>
+			<div class="handle top-right" @mousedown.prevent="onTopRightHandleMousedown"></div>
+			<div class="handle bottom-right" @mousedown.prevent="onBottomRightHandleMousedown"></div>
+			<div class="handle bottom-left" @mousedown.prevent="onBottomLeftHandleMousedown"></div>
+		</template>
 	</div>
 </div>
 </template>
@@ -85,7 +87,7 @@ export default Vue.extend({
 
 	computed: {
 		isFlexible(): boolean {
-			return this.height == null;
+			return this.height == 'auto';
 		},
 		canResize(): boolean {
 			return !this.isFlexible;
@@ -476,7 +478,7 @@ root(isDark)
 		left 0
 		width 100%
 		height 100%
-		background rgba(0, 0, 0, 0.7)
+		background rgba(#000, 0.7)
 		opacity 0
 		pointer-events none
 
@@ -493,7 +495,7 @@ root(isDark)
 		&:focus
 			&:not([data-is-modal])
 				> .body
-					box-shadow 0 0 0px 1px rgba($theme-color, 0.5), 0 2px 6px 0 rgba(0, 0, 0, 0.2)
+					box-shadow 0 0 0px 1px rgba($theme-color, 0.5), 0 2px 6px 0 rgba(#000, 0.2)
 
 		> .handle
 			$size = 8px
@@ -561,7 +563,7 @@ root(isDark)
 			overflow hidden
 			background isDark ? #282C37 : #fff
 			border-radius 6px
-			box-shadow 0 2px 6px 0 rgba(0, 0, 0, 0.2)
+			box-shadow 0 2px 6px 0 rgba(#000, 0.2)
 
 			> header
 				$header-height = 40px
diff --git a/src/client/app/desktop/views/pages/search.vue b/src/client/app/desktop/views/pages/search.vue
index 698154e66..67e1e3bfe 100644
--- a/src/client/app/desktop/views/pages/search.vue
+++ b/src/client/app/desktop/views/pages/search.vue
@@ -114,7 +114,7 @@ export default Vue.extend({
 .notes
 	max-width 600px
 	margin 0 auto
-	border solid 1px rgba(0, 0, 0, 0.075)
+	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 	overflow hidden
 
diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
index 9ccbc7a31..4c1b91e7a 100644
--- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
+++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
@@ -38,7 +38,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 .followers-you-know
 	background #fff
-	border solid 1px rgba(0, 0, 0, 0.075)
+	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 
 	> .title
@@ -49,7 +49,7 @@ export default Vue.extend({
 		font-size 0.9em
 		font-weight bold
 		color #888
-		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+		box-shadow 0 1px rgba(#000, 0.07)
 
 		> i
 			margin-right 4px
diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue
index 203f93647..161b08d1d 100644
--- a/src/client/app/desktop/views/pages/user/user.friends.vue
+++ b/src/client/app/desktop/views/pages/user/user.friends.vue
@@ -44,7 +44,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 .friends
 	background #fff
-	border solid 1px rgba(0, 0, 0, 0.075)
+	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 
 	> .title
@@ -55,7 +55,7 @@ export default Vue.extend({
 		font-size 0.9em
 		font-weight bold
 		color #888
-		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+		box-shadow 0 1px rgba(#000, 0.07)
 
 		> i
 			margin-right 4px
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index 8f72fd716..99fe0b18d 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -72,7 +72,7 @@ export default Vue.extend({
 
 	overflow hidden
 	background #f7f7f7
-	box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
+	box-shadow 0 1px 1px rgba(#000, 0.075)
 
 	> .is-suspended
 	> .is-remote
@@ -99,7 +99,7 @@ export default Vue.extend({
 				background-color #383838
 
 			> .fade
-				background linear-gradient(transparent, rgba(0, 0, 0, 0.7))
+				background linear-gradient(transparent, rgba(#000, 0.7))
 
 		> .container
 			> .title
@@ -142,7 +142,7 @@ export default Vue.extend({
 			margin 0
 			border solid 3px #fff
 			border-radius 8px
-			box-shadow 1px 1px 3px rgba(0, 0, 0, 0.2)
+			box-shadow 1px 1px 3px rgba(#000, 0.2)
 
 		> .title
 			position absolute
diff --git a/src/client/app/desktop/views/pages/user/user.home.vue b/src/client/app/desktop/views/pages/user/user.home.vue
index 7ca520ea7..6b242a612 100644
--- a/src/client/app/desktop/views/pages/user/user.home.vue
+++ b/src/client/app/desktop/views/pages/user/user.home.vue
@@ -65,7 +65,7 @@ export default Vue.extend({
 		width calc(100% - 275px * 2)
 
 		> .timeline
-			border solid 1px rgba(0, 0, 0, 0.075)
+			border solid 1px rgba(#000, 0.075)
 			border-radius 6px
 
 	> div
@@ -91,7 +91,7 @@ export default Vue.extend({
 				font-size 12px
 				color #aaa
 				background #fff
-				border solid 1px rgba(0, 0, 0, 0.075)
+				border solid 1px rgba(#000, 0.075)
 				border-radius 6px
 
 				a
diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue
index 9f749d5cc..01c4c7b31 100644
--- a/src/client/app/desktop/views/pages/user/user.photos.vue
+++ b/src/client/app/desktop/views/pages/user/user.photos.vue
@@ -41,7 +41,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 .photos
 	background #fff
-	border solid 1px rgba(0, 0, 0, 0.075)
+	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 
 	> .title
@@ -52,7 +52,7 @@ export default Vue.extend({
 		font-size 0.9em
 		font-weight bold
 		color #888
-		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+		box-shadow 0 1px rgba(#000, 0.07)
 
 		> i
 			margin-right 4px
diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue
index 64acbd86b..29e49f36a 100644
--- a/src/client/app/desktop/views/pages/user/user.profile.vue
+++ b/src/client/app/desktop/views/pages/user/user.profile.vue
@@ -118,7 +118,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 .profile
 	background #fff
-	border solid 1px rgba(0, 0, 0, 0.075)
+	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 
 	> *:first-child
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 6bda29326..223be8301 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -231,14 +231,14 @@ export default Vue.extend({
 						width 410px
 						background #fff
 						border-radius 8px
-						box-shadow 0 0 0 12px rgba(0, 0, 0, 0.1)
+						box-shadow 0 0 0 12px rgba(#000, 0.1)
 						overflow hidden
 
 						> header
 							z-index 1
 							padding 12px 16px
 							color #888d94
-							box-shadow 0 1px 0px rgba(0, 0, 0, 0.1)
+							box-shadow 0 1px 0px rgba(#000, 0.1)
 
 							> div
 								position absolute
diff --git a/src/client/app/desktop/views/widgets/channel.vue b/src/client/app/desktop/views/widgets/channel.vue
index 7e96f8ee3..600cdd531 100644
--- a/src/client/app/desktop/views/widgets/channel.vue
+++ b/src/client/app/desktop/views/widgets/channel.vue
@@ -61,7 +61,7 @@ export default define({
 <style lang="stylus" scoped>
 .mkw-channel
 	background #fff
-	border solid 1px rgba(0, 0, 0, 0.075)
+	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 	overflow hidden
 
@@ -73,7 +73,7 @@ export default define({
 		font-size 0.9em
 		font-weight bold
 		color #888
-		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+		box-shadow 0 1px rgba(#000, 0.07)
 
 		> [data-fa]
 			margin-right 4px
diff --git a/src/client/app/desktop/views/widgets/post-form.vue b/src/client/app/desktop/views/widgets/post-form.vue
index 627943588..9c2d60f9b 100644
--- a/src/client/app/desktop/views/widgets/post-form.vue
+++ b/src/client/app/desktop/views/widgets/post-form.vue
@@ -59,7 +59,7 @@ export default define({
 .mkw-post-form
 	background #fff
 	overflow hidden
-	border solid 1px rgba(0, 0, 0, 0.075)
+	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 
 	> .title
@@ -70,7 +70,7 @@ export default define({
 		font-size 0.9em
 		font-weight bold
 		color #888
-		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+		box-shadow 0 1px rgba(#000, 0.07)
 
 		> [data-fa]
 			margin-right 4px
diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
index ba66b703b..7958b4829 100644
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ b/src/client/app/desktop/views/widgets/profile.vue
@@ -45,7 +45,7 @@ export default define({
 root(isDark)
 	overflow hidden
 	background isDark ? #282c37 : #fff
-	border solid 1px rgba(0, 0, 0, 0.075)
+	border solid 1px rgba(#000, 0.075)
 	border-radius 6px
 
 	&[data-compact]
@@ -54,14 +54,14 @@ root(isDark)
 			display block
 			width 100%
 			height 100%
-			background rgba(0, 0, 0, 0.5)
+			background rgba(#000, 0.5)
 
 		> .avatar
 			top ((100px - 58px) / 2)
 			left ((100px - 58px) / 2)
 			border none
 			border-radius 100%
-			box-shadow 0 0 16px rgba(0, 0, 0, 0.5)
+			box-shadow 0 0 16px rgba(#000, 0.5)
 
 		> .name
 			position absolute
@@ -70,7 +70,7 @@ root(isDark)
 			margin 0
 			line-height 100px
 			color #fff
-			text-shadow 0 0 8px rgba(0, 0, 0, 0.5)
+			text-shadow 0 0 8px rgba(#000, 0.5)
 
 		> .username
 			display none
diff --git a/src/client/app/mobile/views/components/drive-file-chooser.vue b/src/client/app/mobile/views/components/drive-file-chooser.vue
index 41536afbd..d95d5fa22 100644
--- a/src/client/app/mobile/views/components/drive-file-chooser.vue
+++ b/src/client/app/mobile/views/components/drive-file-chooser.vue
@@ -54,7 +54,7 @@ export default Vue.extend({
 	width 100%
 	height 100%
 	padding 8px
-	background rgba(0, 0, 0, 0.2)
+	background rgba(#000, 0.2)
 
 	> .body
 		width 100%
diff --git a/src/client/app/mobile/views/components/drive-folder-chooser.vue b/src/client/app/mobile/views/components/drive-folder-chooser.vue
index bfd8fbda6..7934fb781 100644
--- a/src/client/app/mobile/views/components/drive-folder-chooser.vue
+++ b/src/client/app/mobile/views/components/drive-folder-chooser.vue
@@ -38,7 +38,7 @@ export default Vue.extend({
 	width 100%
 	height 100%
 	padding 8px
-	background rgba(0, 0, 0, 0.2)
+	background rgba(#000, 0.2)
 
 	> .body
 		width 100%
diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue
index c7be7d187..764822e98 100644
--- a/src/client/app/mobile/views/components/drive.file-detail.vue
+++ b/src/client/app/mobile/views/components/drive.file-detail.vue
@@ -139,7 +139,7 @@ export default Vue.extend({
 			max-width 100%
 			max-height 300px
 			margin 0 auto
-			box-shadow 1px 1px 4px rgba(0, 0, 0, 0.2)
+			box-shadow 1px 1px 4px rgba(#000, 0.2)
 
 		> footer
 			padding 8px 8px 0 8px
@@ -226,7 +226,7 @@ export default Vue.extend({
 					background-color #767676
 					background-image none
 					border-color #444
-					box-shadow 0 1px 3px rgba(0, 0, 0, 0.075), inset 0 0 5px rgba(0, 0, 0, 0.2)
+					box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2)
 
 				> [data-fa]
 					margin-right 4px
diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue
index 7aa666e1b..ef3432a3e 100644
--- a/src/client/app/mobile/views/components/drive.vue
+++ b/src/client/app/mobile/views/components/drive.vue
@@ -474,11 +474,11 @@ export default Vue.extend({
 		overflow auto
 		white-space nowrap
 		font-size 0.9em
-		color rgba(0, 0, 0, 0.67)
+		color rgba(#000, 0.67)
 		-webkit-backdrop-filter blur(12px)
 		backdrop-filter blur(12px)
 		background-color rgba(#fff, 0.75)
-		border-bottom solid 1px rgba(0, 0, 0, 0.13)
+		border-bottom solid 1px rgba(#000, 0.13)
 
 		> p
 		> a
@@ -555,7 +555,7 @@ export default Vue.extend({
 			display inline-block
 			position absolute
 			top 0
-			background rgba(0, 0, 0, 0.2)
+			background rgba(#000, 0.2)
 			border-radius 100%
 
 			animation sk-bounce 2.0s infinite ease-in-out
diff --git a/src/client/app/mobile/views/components/friends-maker.vue b/src/client/app/mobile/views/components/friends-maker.vue
index 961a5f568..ba4abe341 100644
--- a/src/client/app/mobile/views/components/friends-maker.vue
+++ b/src/client/app/mobile/views/components/friends-maker.vue
@@ -57,7 +57,7 @@ export default Vue.extend({
 .mk-friends-maker
 	background #fff
 	border-radius 8px
-	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+	box-shadow 0 0 0 1px rgba(#000, 0.2)
 
 	> .title
 		margin 0
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index 3c7d225ca..36c3f30c6 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -35,6 +35,7 @@
 		</header>
 		<div class="body">
 			<div class="text">
+				<span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
 				<mk-note-html v-if="p.text" :text="p.text" :i="os.i"/>
 			</div>
 			<div class="tags" v-if="p.tags && p.tags.length > 0">
diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue
index a0d893864..8ab66940c 100644
--- a/src/client/app/mobile/views/components/notifications.vue
+++ b/src/client/app/mobile/views/components/notifications.vue
@@ -107,10 +107,11 @@ root(isDark)
 	margin 0 auto
 	background isDark ? #282C37 :#fff
 	border-radius 8px
-	box-shadow 0 0 2px rgba(0, 0, 0, 0.1)
+	box-shadow 0 0 2px rgba(#000, 0.1)
+	overflow hidden
 
 	@media (min-width 500px)
-		box-shadow 0 8px 32px rgba(0, 0, 0, 0.1)
+		box-shadow 0 8px 32px rgba(#000, 0.1)
 
 	.transition
 		.mk-notifications-enter
@@ -147,7 +148,7 @@ root(isDark)
 		width 100%
 		padding 16px
 		color #555
-		border-top solid 1px rgba(0, 0, 0, 0.05)
+		border-top solid 1px rgba(#000, 0.05)
 
 		> [data-fa]
 			margin-right 4px
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index 7b8a52f06..ec1611979 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -10,6 +10,10 @@
 	</header>
 	<div class="form">
 		<mk-note-preview v-if="reply" :note="reply"/>
+		<div v-if="visibility == 'specified'" class="visibleUsers">
+			<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
+			<a @click="addVisibleUser">+ユーザーを追加</a>
+		</div>
 		<input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)">
 		<textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:!@reply-placeholder%' : '%i18n:!@note-placeholder%'"></textarea>
 		<div class="attaches" v-show="files.length != 0">
@@ -27,6 +31,7 @@
 		<button class="poll" @click="poll = true">%fa:chart-pie%</button>
 		<button class="poll" @click="useCw = !useCw">%fa:eye-slash%</button>
 		<button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
+		<button class="visibility" @click="setVisibility" ref="visibilityButton">%fa:lock%</button>
 		<input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
 	</div>
 </div>
@@ -35,11 +40,13 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as XDraggable from 'vuedraggable';
+import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
 import getKao from '../../../common/scripts/get-kao';
 
 export default Vue.extend({
 	components: {
-		XDraggable
+		XDraggable,
+		MkVisibilityChooser
 	},
 
 	props: ['reply'],
@@ -52,6 +59,8 @@ export default Vue.extend({
 			files: [],
 			poll: false,
 			geo: null,
+			visibility: 'public',
+			visibleUsers: [],
 			useCw: false,
 			cw: null
 		};
@@ -121,6 +130,33 @@ export default Vue.extend({
 			this.geo = null;
 		},
 
+		setVisibility() {
+			const w = (this as any).os.new(MkVisibilityChooser, {
+				source: this.$refs.visibilityButton,
+				compact: true,
+				v: this.visibility
+			});
+			w.$once('chosen', v => {
+				this.visibility = v;
+			});
+		},
+
+		addVisibleUser() {
+			(this as any).apis.input({
+				title: 'ユーザー名を入力してください'
+			}).then(username => {
+				(this as any).api('users/show', {
+					username
+				}).then(user => {
+					this.visibleUsers.push(user);
+				});
+			});
+		},
+
+		removeVisibleUser(user) {
+			this.visibleUsers = this.visibleUsers.filter(u => u != user);
+		},
+
 		clear() {
 			this.text = '';
 			this.files = [];
@@ -145,6 +181,8 @@ export default Vue.extend({
 					heading: isNaN(this.geo.heading) ? null : this.geo.heading,
 					speed: this.geo.speed,
 				} : null,
+				visibility: this.visibility,
+				visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
 				viaMobile: viaMobile
 			}).then(data => {
 				this.$emit('note');
@@ -169,33 +207,33 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
-.mk-post-form
+root(isDark)
 	max-width 500px
 	width calc(100% - 16px)
 	margin 8px auto
-	background #fff
+	background isDark ? #282C37 : #fff
 	border-radius 8px
-	box-shadow 0 0 2px rgba(0, 0, 0, 0.1)
+	box-shadow 0 0 2px rgba(#000, 0.1)
 
 	@media (min-width 500px)
 		margin 16px auto
 		width calc(100% - 32px)
-		box-shadow 0 8px 32px rgba(0, 0, 0, 0.1)
+		box-shadow 0 8px 32px rgba(#000, 0.1)
 
 	@media (min-width 600px)
 		margin 32px auto
 
 	> header
-		z-index 1
+		z-index 1000
 		height 50px
-		box-shadow 0 1px 0 0 rgba(0, 0, 0, 0.1)
+		box-shadow 0 1px 0 0 isDark ? rgba(#000, 0.2) : rgba(#000, 0.1)
 
 		> .cancel
 			padding 0
 			width 50px
 			line-height 50px
 			font-size 24px
-			color #555
+			color isDark ? #9baec8 : #555
 
 		> div
 			position absolute
@@ -229,6 +267,38 @@ export default Vue.extend({
 		> .mk-note-preview
 			padding 16px
 
+		> .visibleUsers
+			margin-bottom 8px
+			font-size 14px
+
+			> span
+				margin-right 16px
+				color isDark ? #fff : #666
+
+		> input
+			z-index 1
+
+		> input
+		> textarea
+			display block
+			padding 12px
+			margin 0
+			width 100%
+			font-size 16px
+			color isDark ? #fff : #333
+			background isDark ? #191d23 : #fff
+			border none
+			border-radius 0
+			box-shadow 0 1px 0 0 isDark ? rgba(#000, 0.2) : rgba(#000, 0.1)
+
+			&:disabled
+				opacity 0.5
+
+		> textarea
+			max-width 100%
+			min-width 100%
+			min-height 80px
+
 		> .attaches
 
 			> .files
@@ -262,31 +332,12 @@ export default Vue.extend({
 		> .file
 			display none
 
-		> input
-		> textarea
-			display block
-			padding 12px
-			margin 0
-			width 100%
-			font-size 16px
-			color #333
-			border none
-			border-bottom solid 1px #ddd
-			border-radius 0
-
-			&:disabled
-				opacity 0.5
-
-		> textarea
-			max-width 100%
-			min-width 100%
-			min-height 80px
-
 		> .upload
 		> .drive
 		> .kao
 		> .poll
 		> .geo
+		> .visibility
 			display inline-block
 			padding 0
 			margin 0
@@ -300,5 +351,10 @@ export default Vue.extend({
 			border-radius 0
 			box-shadow none
 
-</style>
+.mk-post-form[data-darkmode]
+	root(true)
 
+.mk-post-form:not([data-darkmode])
+	root(false)
+
+</style>
diff --git a/src/client/app/mobile/views/components/users-list.vue b/src/client/app/mobile/views/components/users-list.vue
index 67a38a895..617506745 100644
--- a/src/client/app/mobile/views/components/users-list.vue
+++ b/src/client/app/mobile/views/components/users-list.vue
@@ -74,7 +74,7 @@ export default Vue.extend({
 		justify-content center
 		margin 0 auto
 		max-width 600px
-		border-bottom solid 1px rgba(0, 0, 0, 0.2)
+		border-bottom solid 1px rgba(#000, 0.2)
 
 		> span
 			display block
@@ -97,7 +97,7 @@ export default Vue.extend({
 				font-size 12px
 				line-height 1
 				color #fff
-				background rgba(0, 0, 0, 0.3)
+				background rgba(#000, 0.3)
 				border-radius 20px
 
 	> .users
@@ -106,14 +106,14 @@ export default Vue.extend({
 		width calc(100% - 16px)
 		background #fff
 		border-radius 8px
-		box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+		box-shadow 0 0 0 1px rgba(#000, 0.2)
 
 		@media (min-width 500px)
 			margin 16px auto
 			width calc(100% - 32px)
 
 		> *
-			border-bottom solid 1px rgba(0, 0, 0, 0.05)
+			border-bottom solid 1px rgba(#000, 0.05)
 
 	> .no
 		margin 0
diff --git a/src/client/app/mobile/views/components/widget-container.vue b/src/client/app/mobile/views/components/widget-container.vue
index 7319c9084..1bdc87576 100644
--- a/src/client/app/mobile/views/components/widget-container.vue
+++ b/src/client/app/mobile/views/components/widget-container.vue
@@ -28,7 +28,7 @@ export default Vue.extend({
 .mk-widget-container
 	background #eee
 	border-radius 8px
-	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+	box-shadow 0 0 0 1px rgba(#000, 0.2)
 	overflow hidden
 
 	&.hideHeader
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index e43c5876e..f69c4d9c6 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -121,7 +121,7 @@ root(isDark)
 			margin 0 auto
 			background isDark ? #272f3a : #fff
 			border-radius 8px
-			box-shadow 0 0 16px rgba(0, 0, 0, 0.1)
+			box-shadow 0 0 16px rgba(#000, 0.1)
 
 			$balloon-size = 16px
 
diff --git a/src/client/app/mobile/views/pages/profile-setting.vue b/src/client/app/mobile/views/pages/profile-setting.vue
index d0b9095ce..7048cdef3 100644
--- a/src/client/app/mobile/views/pages/profile-setting.vue
+++ b/src/client/app/mobile/views/pages/profile-setting.vue
@@ -136,7 +136,7 @@ export default Vue.extend({
 .form
 	position relative
 	background #fff
-	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+	box-shadow 0 0 0 1px rgba(#000, 0.2)
 	border-radius 8px
 
 	&:before
@@ -145,7 +145,7 @@ export default Vue.extend({
 		position absolute
 		bottom -20px
 		left calc(50% - 10px)
-		border-top solid 10px rgba(0, 0, 0, 0.2)
+		border-top solid 10px rgba(#000, 0.2)
 		border-right solid 10px transparent
 		border-bottom solid 10px transparent
 		border-left solid 10px transparent
diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue
index 6c80de3aa..f038a6f81 100644
--- a/src/client/app/mobile/views/pages/search.vue
+++ b/src/client/app/mobile/views/pages/search.vue
@@ -84,7 +84,7 @@ export default Vue.extend({
 	width calc(100% - 16px)
 	background #fff
 	border-radius 8px
-	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+	box-shadow 0 0 0 1px rgba(#000, 0.2)
 
 	@media (min-width 500px)
 		margin 16px auto
diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue
index 741559ed0..d730e4fcf 100644
--- a/src/client/app/mobile/views/pages/selectdrive.vue
+++ b/src/client/app/mobile/views/pages/selectdrive.vue
@@ -62,7 +62,7 @@ export default Vue.extend({
 		width 100%
 		z-index 1000
 		background #fff
-		box-shadow 0 1px rgba(0, 0, 0, 0.1)
+		box-shadow 0 1px rgba(#000, 0.1)
 
 		> h1
 			margin 0
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index ebf14f68f..0e9c5ea96 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -62,7 +62,7 @@ export default Vue.extend({
 		width calc(100% - 32px)
 		list-style none
 		background #fff
-		border solid 1px rgba(0, 0, 0, 0.2)
+		border solid 1px rgba(#000, 0.2)
 		border-radius $radius
 
 		> li
@@ -70,7 +70,7 @@ export default Vue.extend({
 			border-bottom solid 1px #ddd
 
 			&:hover
-				background rgba(0, 0, 0, 0.1)
+				background rgba(#000, 0.1)
 
 			&:first-child
 				border-top-left-radius $radius
diff --git a/src/client/app/mobile/views/pages/signup.vue b/src/client/app/mobile/views/pages/signup.vue
index 9dc07a4b8..b8245beb0 100644
--- a/src/client/app/mobile/views/pages/signup.vue
+++ b/src/client/app/mobile/views/pages/signup.vue
@@ -40,7 +40,7 @@ export default Vue.extend({
 
 	.form
 		background #fff
-		border solid 1px rgba(0, 0, 0, 0.2)
+		border solid 1px rgba(#000, 0.2)
 		border-radius 8px
 		overflow hidden
 
diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
index 1bc7ffd86..485996870 100644
--- a/src/client/app/mobile/views/pages/welcome.vue
+++ b/src/client/app/mobile/views/pages/welcome.vue
@@ -108,7 +108,7 @@ export default Vue.extend({
 		.form
 			margin-bottom 16px
 			background #fff
-			border solid 1px rgba(0, 0, 0, 0.2)
+			border solid 1px rgba(#000, 0.2)
 			border-radius 8px
 			overflow hidden
 
@@ -131,7 +131,7 @@ export default Vue.extend({
 						margin 0 0 16px 0
 						width 100%
 						font-size 1em
-						color rgba(0, 0, 0, 0.7)
+						color rgba(#000, 0.7)
 						background #fff
 						outline none
 						border solid 1px #ddd
@@ -156,7 +156,7 @@ export default Vue.extend({
 							background-color #767676
 							background-image none
 							border-color #444
-							box-shadow 0 1px 3px rgba(0, 0, 0, 0.075), inset 0 0 5px rgba(0, 0, 0, 0.2)
+							box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2)
 
 				> div
 					padding 16px
@@ -164,7 +164,7 @@ export default Vue.extend({
 
 		> .tl
 			background #fff
-			border solid 1px rgba(0, 0, 0, 0.2)
+			border solid 1px rgba(#000, 0.2)
 			border-radius 8px
 			overflow hidden
 
diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue
index 502f886ce..59c1ec7c0 100644
--- a/src/client/app/mobile/views/widgets/profile.vue
+++ b/src/client/app/mobile/views/widgets/profile.vue
@@ -34,7 +34,7 @@ export default define({
 	display block
 	width 100%
 	height 100%
-	background rgba(0, 0, 0, 0.5)
+	background rgba(#000, 0.5)
 
 .avatar
 	display block
@@ -47,7 +47,7 @@ export default define({
 	left ((100px - 58px) / 2)
 	border none
 	border-radius 100%
-	box-shadow 0 0 16px rgba(0, 0, 0, 0.5)
+	box-shadow 0 0 16px rgba(#000, 0.5)
 
 .name
 	display block
@@ -58,6 +58,6 @@ export default define({
 	line-height 100px
 	color #fff
 	font-weight bold
-	text-shadow 0 0 8px rgba(0, 0, 0, 0.5)
+	text-shadow 0 0 8px rgba(#000, 0.5)
 
 </style>
diff --git a/src/renderers/get-note-summary.ts b/src/renderers/get-note-summary.ts
index fc7482ca1..dfc05ebfd 100644
--- a/src/renderers/get-note-summary.ts
+++ b/src/renderers/get-note-summary.ts
@@ -3,6 +3,10 @@
  * @param {*} note 投稿
  */
 const summarize = (note: any): string => {
+	if (note.isHidden) {
+		return '(非公開の投稿)';
+	}
+
 	let summary = '';
 
 	// チャンネル

From 9870e73069e06370a4cc20b65bd1230c1415b67a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 29 Apr 2018 09:00:41 +0900
Subject: [PATCH 10/12] wip

---
 .../desktop/views/components/post-form.vue    | 14 +++++++++++++
 .../mobile/views/components/note-preview.vue  | 20 ++++++++++---------
 2 files changed, 25 insertions(+), 9 deletions(-)

diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index 92a7f9ada..ec220cc58 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -35,6 +35,7 @@
 	<button class="poll" title="内容を隠す" @click="useCw = !useCw">%fa:eye-slash%</button>
 	<button class="geo" title="位置情報を添付する" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
 	<button class="visibility" title="公開範囲" @click="setVisibility" ref="visibilityButton">%fa:lock%</button>
+	<p class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</p>
 	<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
 		{{ posting ? '%i18n:!@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
 	</button>
@@ -557,6 +558,19 @@ root(isDark)
 				from {background-position: 0 0;}
 				to   {background-position: -64px 32px;}
 
+	> .text-count
+		pointer-events none
+		display block
+		position absolute
+		bottom 16px
+		right 138px
+		margin 0
+		line-height 40px
+		color rgba($theme-color, 0.5)
+
+		&.over
+			color #ec3828
+
 	> .upload
 	> .drive
 	> .kao
diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
index 1de739d5d..41244cc75 100644
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -27,7 +27,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-note-preview
+root(isDark)
 	margin 0
 	padding 0
 	font-size 0.9em
@@ -37,10 +37,6 @@ export default Vue.extend({
 		display block
 		clear both
 
-	&:hover
-		> .main > footer > button
-			color #888
-
 	> .avatar-anchor
 		display block
 		float left
@@ -68,7 +64,7 @@ export default Vue.extend({
 				margin 0 .5em 0 0
 				padding 0
 				overflow hidden
-				color #607073
+				color isDark ? #fff : #607073
 				font-size 1em
 				font-weight 700
 				text-align left
@@ -81,11 +77,11 @@ export default Vue.extend({
 			> .username
 				text-align left
 				margin 0 .5em 0 0
-				color #d1d8da
+				color isDark ? #606984 : #d1d8da
 
 			> .time
 				margin-left auto
-				color #b2b8bb
+				color isDark ? #606984 : #b2b8bb
 
 		> .body
 
@@ -94,6 +90,12 @@ export default Vue.extend({
 				margin 0
 				padding 0
 				font-size 1.1em
-				color #717171
+				color isDark ? #959ba7 : #717171
+
+.mk-note-preview[data-darkmode]
+	root(true)
+
+.mk-note-preview:not([data-darkmode])
+	root(false)
 
 </style>

From 2f2a94590599185c09ddbfa71efd99213e7eaa91 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 29 Apr 2018 09:19:05 +0900
Subject: [PATCH 11/12] wip

---
 .../views/components/notes.note.sub.vue       | 28 +++++++++++++++----
 .../desktop/views/components/notes.note.vue   | 15 +++++++---
 .../app/mobile/views/components/note.sub.vue  | 28 +++++++++++++++----
 .../app/mobile/views/components/note.vue      | 16 ++++++++---
 4 files changed, 69 insertions(+), 18 deletions(-)

diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue
index 3e1b75c29..4472ddefb 100644
--- a/src/client/app/desktop/views/components/notes.note.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -7,9 +7,18 @@
 		<header>
 			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
 			<span class="username">@{{ note.user | acct }}</span>
-			<router-link class="created-at" :to="note | notePage">
-				<mk-time :time="note.createdAt"/>
-			</router-link>
+			<div class="info">
+				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
+				<router-link class="created-at" :to="note | notePage">
+					<mk-time :time="note.createdAt"/>
+				</router-link>
+				<span class="visibility" v-if="note.visibility != 'public'">
+					<template v-if="note.visibility == 'home'">%fa:home%</template>
+					<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
+					<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
+					<template v-if="note.visibility == 'private'">%fa:lock%</template>
+				</span>
+			</div>
 		</header>
 		<div class="body">
 			<mk-sub-note-content class="text" :note="note"/>
@@ -85,9 +94,18 @@ root(isDark)
 				margin 0 .5em 0 0
 				color isDark ? #606984 : #d1d8da
 
-			> .created-at
+			> .info
 				margin-left auto
-				color isDark ? #606984 : #b2b8bb
+				font-size 0.9em
+
+				> *
+					color isDark ? #606984 : #b2b8bb
+
+				> .mobile
+					margin-right 6px
+
+				> .visibility
+					margin-left 6px
 
 		> .body
 			max-height 128px
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 4d7e6ee8b..ee24543eb 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -28,6 +28,12 @@
 					<router-link class="created-at" :to="p | notePage">
 						<mk-time :time="p.createdAt"/>
 					</router-link>
+					<span class="visibility" v-if="p.visibility != 'public'">
+						<template v-if="p.visibility == 'home'">%fa:home%</template>
+						<template v-if="p.visibility == 'followers'">%fa:unlock%</template>
+						<template v-if="p.visibility == 'specified'">%fa:envelope%</template>
+						<template v-if="p.visibility == 'private'">%fa:lock%</template>
+					</span>
 				</div>
 			</header>
 			<div class="body">
@@ -442,18 +448,19 @@ root(isDark)
 					margin-left auto
 					font-size 0.9em
 
+					> *
+						color isDark ? #606984 : #c0c0c0
+
 					> .mobile
 						margin-right 8px
-						color isDark ? #606984 : #ccc
 
 					> .app
 						margin-right 8px
 						padding-right 8px
-						color #ccc
 						border-right solid 1px #eaeaea
 
-					> .created-at
-						color isDark ? #606984 : #c0c0c0
+					> .visibility
+						margin-left 8px
 
 			> .body
 
diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue
index 8e3835ac2..01f02bdb5 100644
--- a/src/client/app/mobile/views/components/note.sub.vue
+++ b/src/client/app/mobile/views/components/note.sub.vue
@@ -7,9 +7,18 @@
 		<header>
 			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
 			<span class="username">@{{ note.user | acct }}</span>
-			<router-link class="created-at" :to="note | notePage">
-				<mk-time :time="note.createdAt"/>
-			</router-link>
+			<div class="info">
+				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
+				<router-link class="created-at" :to="note | notePage">
+					<mk-time :time="note.createdAt"/>
+				</router-link>
+				<span class="visibility" v-if="note.visibility != 'public'">
+					<template v-if="note.visibility == 'home'">%fa:home%</template>
+					<template v-if="note.visibility == 'followers'">%fa:unlock%</template>
+					<template v-if="note.visibility == 'specified'">%fa:envelope%</template>
+					<template v-if="note.visibility == 'private'">%fa:lock%</template>
+				</span>
+			</div>
 		</header>
 		<div class="body">
 			<mk-sub-note-content class="text" :note="note"/>
@@ -92,9 +101,18 @@ root(isDark)
 				margin 0
 				color isDark ? #606984 : #d1d8da
 
-			> .created-at
+			> .info
 				margin-left auto
-				color isDark ? #606984 : #b2b8bb
+				font-size 0.9em
+
+				> *
+					color isDark ? #606984 : #b2b8bb
+
+				> .mobile
+					margin-right 6px
+
+				> .visibility
+					margin-left 6px
 
 		> .body
 			max-height 128px
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index 5202e0eb5..07e18544d 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -27,6 +27,12 @@
 					<router-link class="created-at" :to="p | notePage">
 						<mk-time :time="p.createdAt"/>
 					</router-link>
+					<span class="visibility" v-if="p.visibility != 'public'">
+						<template v-if="p.visibility == 'home'">%fa:home%</template>
+						<template v-if="p.visibility == 'followers'">%fa:unlock%</template>
+						<template v-if="p.visibility == 'specified'">%fa:envelope%</template>
+						<template v-if="p.visibility == 'private'">%fa:lock%</template>
+					</span>
 				</div>
 			</header>
 			<div class="body">
@@ -379,12 +385,14 @@ root(isDark)
 					margin-left auto
 					font-size 0.9em
 
-					> .mobile
-						margin-right 6px
+					> *
 						color isDark ? #606984 : #c0c0c0
 
-					> .created-at
-						color isDark ? #606984 : #c0c0c0
+					> .mobile
+						margin-right 6px
+
+					> .visibility
+						margin-left 6px
 
 			> .body
 

From 72c1352acf7b3c6c2e0097ba9255d0ce1a14a867 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 29 Apr 2018 09:29:39 +0900
Subject: [PATCH 12/12] wip

---
 src/services/note/create.ts | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 8d6626713..c2c03516e 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -123,7 +123,7 @@ export default async (user: IUser, data: {
 	if (note.channelId == null) {
 		if (!silent) {
 			if (isLocalUser(user)) {
-				if (note.visibility == 'private') {
+				if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') {
 					// Publish event to myself's stream
 					stream(note.userId, 'note', await pack(note, user, {
 						detail: true
@@ -133,7 +133,9 @@ export default async (user: IUser, data: {
 					stream(note.userId, 'note', noteObj);
 
 					// Publish note to local timeline stream
-					publishLocalTimelineStream(noteObj);
+					if (note.visibility != 'home') {
+						publishLocalTimelineStream(noteObj);
+					}
 				}
 			}