From f2fea7f3cd5a131f994ad9bc1d2b4833865dd165 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 20 Apr 2018 03:41:24 +0900
Subject: [wip] darkmode

---
 .../desktop/views/components/widget-container.vue  | 23 +++++++++++++++-------
 1 file changed, 16 insertions(+), 7 deletions(-)

(limited to 'src/client/app/desktop/views/components/widget-container.vue')

diff --git a/src/client/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue
index 188a67313e..c3fac1399d 100644
--- a/src/client/app/desktop/views/components/widget-container.vue
+++ b/src/client/app/desktop/views/components/widget-container.vue
@@ -34,8 +34,8 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-widget-container
-	background #fff
+root(isDark)
+	background isDark ? #282C37 : #fff
 	border solid 1px rgba(0, 0, 0, 0.075)
 	border-radius 6px
 	overflow hidden
@@ -45,6 +45,8 @@ export default Vue.extend({
 		border none !important
 
 	> header
+		background isDark ? #313543 : #fff
+
 		> .title
 			z-index 1
 			margin 0
@@ -52,7 +54,7 @@ export default Vue.extend({
 			line-height 42px
 			font-size 0.9em
 			font-weight bold
-			color #888
+			color isDark ? #e3e5e8 : #888
 			box-shadow 0 1px rgba(0, 0, 0, 0.07)
 
 			> [data-fa]
@@ -70,16 +72,23 @@ export default Vue.extend({
 			width 42px
 			font-size 0.9em
 			line-height 42px
-			color #ccc
+			color isDark ? #9baec8 : #ccc
 
 			&:hover
-				color #aaa
+				color isDark ? #b2c1d5 : #aaa
 
 			&:active
-				color #999
+				color isDark ? #b2c1d5 : #999
 
 		&.withGradient
 			> .title
-				background linear-gradient(to bottom, #fff, #ececec)
+				background isDark ? linear-gradient(to bottom, #313543, #1d2027) : linear-gradient(to bottom, #fff, #ececec)
 				box-shadow 0 1px rgba(#000, 0.11)
+
+.mk-widget-container[data-darkmode]
+	root(true)
+
+.mk-widget-container:not([data-darkmode])
+	root(false)
+
 </style>
-- 
cgit v1.2.3-freya


From 80cfa0f95d7b1e49fd4380e0d662ff892789925f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 25 Apr 2018 22:50:59 +0900
Subject: wip

---
 src/client/app/desktop/views/components/widget-container.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/app/desktop/views/components/widget-container.vue')

diff --git a/src/client/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue
index c3fac1399d..926d7702b9 100644
--- a/src/client/app/desktop/views/components/widget-container.vue
+++ b/src/client/app/desktop/views/components/widget-container.vue
@@ -58,7 +58,7 @@ root(isDark)
 			box-shadow 0 1px rgba(0, 0, 0, 0.07)
 
 			> [data-fa]
-				margin-right 4px
+				margin-right 6px
 
 			&:empty
 				display none
-- 
cgit v1.2.3-freya


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: wip

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

(limited to 'src/client/app/desktop/views/components/widget-container.vue')

diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue
index b323907eb0..152b900429 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 e1e1b265e1..0fcd9bfe53 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 5c8f61a2a2..84173d20b5 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 afd700e777..70df899f5a 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 38202d7581..a45114e6bb 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 6f799b34b6..6f8fcb3a70 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 3e4be425d9..88dc22aaf4 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 de4373f561..46e41cbcdb 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 25f90a2f13..7fb9fc3fd4 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 33a559ff8f..516979acd0 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 93758102de..d573db32e6 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 dd36d32e74..50f0877ae9 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 a80bc04f7f..3497976901 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 637ba328c6..0b1c7fe2dd 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 b3b5b3583a..0bb503759c 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 1063c2c279..e35462611d 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 1a63362521..f0ba11678e 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 16859fe07f..532041ae74 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 ad32299f37..95be4b94fd 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 8b43536c2b..e488571070 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 483a4c718b..1d8cc4f3a9 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 3d884e0b8a..843604a059 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 d7548c4415..60a33f9c93 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 fa17e4a9d2..aff21c1754 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 a683af68a7..39881711fa 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 aebface112..973df1014d 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 c54a7db29d..4a5a0f23dc 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 5c5b462176..4343a7fb78 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 dec140d1c9..026522d907 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 cbf862cd1c..959cefa42c 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 53d08a0eca..66bdab5c08 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 fb33421309..5175c8bd45 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 eb12ea109c..525023349e 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 e8fabe3c63..4d7e6ee8b8 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 47463b8b5b..36e9dce6af 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 d704cdc3ca..92a7f9ada7 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 2047b5dced..bcfce299f4 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 5148c5b967..145666b4bc 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 5326f92836..ea814dd7a3 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 3167aab8ab..1ed28ba3a8 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 7ce85d4c3d..353fd58561 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 2e1870800f..d0111d7dc5 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 e8f4c94d42..13d0d07bbc 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 926d7702b9..2edba5a230 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 3de1413ac4..c9820f8697 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 698154e667..67e1e3bfe0 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 9ccbc7a310..4c1b91e7a6 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 203f936478..161b08d1de 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 8f72fd7165..99fe0b18d0 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 7ca520ea7f..6b242a6129 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 9f749d5cc9..01c4c7b31e 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 64acbd86b3..29e49f36a6 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 6bda29326f..223be8301b 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 7e96f8ee3d..600cdd5318 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 627943588f..9c2d60f9b9 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 ba66b703b8..7958b4829a 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 41536afbd4..d95d5fa223 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 bfd8fbda6f..7934fb7816 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 c7be7d1879..764822e98c 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 7aa666e1bb..ef3432a3ec 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 961a5f568a..ba4abe341f 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 3c7d225ca7..36c3f30c62 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 a0d8938646..8ab66940c4 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 7b8a52f06e..ec16119797 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 67a38a8955..6175067459 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 7319c90849..1bdc875763 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 e43c5876e6..f69c4d9c68 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 d0b9095cef..7048cdef31 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 6c80de3aa0..f038a6f81f 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 741559ed0b..d730e4fcff 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 ebf14f68f5..0e9c5ea962 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 9dc07a4b86..b8245beb00 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 1bc7ffd862..485996870e 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 502f886ceb..59c1ec7c0e 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 fc7482ca16..dfc05ebfd6 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 = '';
 
 	// チャンネル
-- 
cgit v1.2.3-freya


From 372bfaceda5bf694bf33986b5a64a56e5787104c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 29 Apr 2018 17:17:15 +0900
Subject: リファクタリングなど
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/client/app/common/define-widget.ts             | 62 ++++++++-------
 src/client/app/common/mios.ts                      | 56 +++++++++-----
 src/client/app/common/scripts/streaming/home.ts    | 23 +++++-
 src/client/app/common/views/components/avatar.vue  | 38 +++++++++
 src/client/app/common/views/components/index.ts    |  2 +
 .../views/components/messaging-room.message.vue    | 24 ++----
 .../app/common/views/components/messaging.vue      |  4 +-
 .../common/views/components/welcome-timeline.vue   | 15 ++--
 src/client/app/common/views/widgets/access-log.vue |  1 +
 src/client/app/common/views/widgets/broadcast.vue  |  1 +
 src/client/app/common/views/widgets/calendar.vue   |  1 +
 .../app/common/views/widgets/photo-stream.vue      |  2 +
 src/client/app/common/views/widgets/rss.vue        |  1 +
 src/client/app/common/views/widgets/server.vue     |  2 +
 src/client/app/common/views/widgets/slideshow.vue  |  2 +
 .../app/desktop/views/components/friends-maker.vue | 17 ++--
 src/client/app/desktop/views/components/home.vue   | 83 +++++++-------------
 .../desktop/views/components/note-detail.sub.vue   | 17 ++--
 .../app/desktop/views/components/note-detail.vue   | 37 +++------
 .../app/desktop/views/components/note-preview.vue  | 17 ++--
 .../desktop/views/components/notes.note.sub.vue    | 17 ++--
 .../app/desktop/views/components/notes.note.vue    | 38 ++++-----
 src/client/app/desktop/views/components/notes.vue  |  6 +-
 .../app/desktop/views/components/notifications.vue | 41 +++-------
 .../app/desktop/views/components/settings.vue      | 59 +++++++-------
 .../app/desktop/views/components/timeline.core.vue |  8 +-
 .../desktop/views/components/ui.header.account.vue |  2 +-
 .../views/components/user-list-timeline.vue        |  8 +-
 .../app/desktop/views/components/user-preview.vue  | 16 ++--
 .../desktop/views/components/users-list.item.vue   | 17 ++--
 .../desktop/views/components/widget-container.vue  |  4 +-
 src/client/app/desktop/views/components/window.vue |  4 +-
 .../app/desktop/views/pages/user-list.users.vue    | 17 ++--
 .../app/desktop/views/pages/user/user.friends.vue  | 17 ++--
 .../app/desktop/views/pages/user/user.header.vue   |  3 +-
 src/client/app/desktop/views/pages/welcome.vue     |  4 +-
 src/client/app/desktop/views/widgets/activity.vue  |  2 +
 src/client/app/desktop/views/widgets/channel.vue   |  1 +
 src/client/app/desktop/views/widgets/messaging.vue |  1 +
 .../app/desktop/views/widgets/notifications.vue    |  1 +
 src/client/app/desktop/views/widgets/polls.vue     |  1 +
 src/client/app/desktop/views/widgets/post-form.vue |  1 +
 src/client/app/desktop/views/widgets/profile.vue   |  1 +
 .../app/desktop/views/widgets/timemachine.vue      |  1 +
 src/client/app/desktop/views/widgets/trends.vue    |  1 +
 src/client/app/desktop/views/widgets/users.vue     | 18 ++---
 src/client/app/init.ts                             | 28 ++-----
 .../mobile/views/components/note-detail.sub.vue    | 17 ++--
 .../app/mobile/views/components/note-detail.vue    | 47 ++++-------
 .../app/mobile/views/components/note-preview.vue   | 17 ++--
 .../app/mobile/views/components/note.sub.vue       | 23 ++----
 src/client/app/mobile/views/components/note.vue    | 46 ++++-------
 src/client/app/mobile/views/components/notes.vue   |  6 +-
 .../app/mobile/views/components/notification.vue   | 32 +++-----
 .../app/mobile/views/components/post-form.vue      |  2 +-
 .../mobile/views/components/user-list-timeline.vue |  8 +-
 .../app/mobile/views/components/user-preview.vue   | 25 ++----
 src/client/app/mobile/views/pages/dashboard.vue    | 20 ++---
 .../app/mobile/views/pages/home.timeline.vue       |  8 +-
 src/client/app/mobile/views/pages/welcome.vue      |  4 +-
 src/client/app/mobile/views/widgets/activity.vue   |  1 +
 src/client/app/store.ts                            | 90 ++++++++++++++++++++++
 .../api/endpoints/i/update_client_setting.ts       | 17 ++--
 src/server/index.ts                                |  6 +-
 64 files changed, 521 insertions(+), 570 deletions(-)
 create mode 100644 src/client/app/common/views/components/avatar.vue
 create mode 100644 src/client/app/store.ts

(limited to 'src/client/app/desktop/views/components/widget-container.vue')

diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts
index 7b98c0903f..0b2bc36566 100644
--- a/src/client/app/common/define-widget.ts
+++ b/src/client/app/common/define-widget.ts
@@ -18,61 +18,65 @@ export default function<T extends object>(data: {
 				default: false
 			}
 		},
+
 		computed: {
 			id(): string {
 				return this.widget.id;
+			},
+
+			props(): T {
+				return this.widget.data;
 			}
 		},
+
 		data() {
 			return {
-				props: data.props ? data.props() : {} as T,
-				bakedOldProps: null,
-				preventSave: false
+				bakedOldProps: null
 			};
 		},
+
 		created() {
-			if (this.props) {
-				Object.keys(this.props).forEach(prop => {
-					if (this.widget.data.hasOwnProperty(prop)) {
-						this.props[prop] = this.widget.data[prop];
-					}
-				});
-			}
+			this.mergeProps();
+
+			this.$watch('props', () => {
+				this.mergeProps();
+			});
 
 			this.bakeProps();
+		},
+
+		methods: {
+			bakeProps() {
+				this.bakedOldProps = JSON.stringify(this.props);
+			},
 
-			this.$watch('props', newProps => {
-				if (this.preventSave) {
-					this.preventSave = false;
-					this.bakeProps();
-					return;
+			mergeProps() {
+				if (data.props) {
+					const defaultProps = data.props();
+					Object.keys(defaultProps).forEach(prop => {
+						if (!this.props.hasOwnProperty(prop)) {
+							Vue.set(this.props, prop, defaultProps[prop]);
+						}
+					});
 				}
-				if (this.bakedOldProps == JSON.stringify(newProps)) return;
+			},
+
+			save() {
+				if (this.bakedOldProps == JSON.stringify(this.props)) return;
 
 				this.bakeProps();
 
 				if (this.isMobile) {
 					(this as any).api('i/update_mobile_home', {
 						id: this.id,
-						data: newProps
-					}).then(() => {
-						(this as any).os.i.clientSettings.mobileHome.find(w => w.id == this.id).data = newProps;
+						data: this.props
 					});
 				} else {
 					(this as any).api('i/update_home', {
 						id: this.id,
-						data: newProps
-					}).then(() => {
-						(this as any).os.i.clientSettings.home.find(w => w.id == this.id).data = newProps;
+						data: this.props
 					});
 				}
-			}, {
-				deep: true
-			});
-		},
-		methods: {
-			bakeProps() {
-				this.bakedOldProps = JSON.stringify(this.props);
 			}
 		}
 	});
diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts
index 4e471cf96f..7dcae47946 100644
--- a/src/client/app/common/mios.ts
+++ b/src/client/app/common/mios.ts
@@ -3,6 +3,7 @@ import { EventEmitter } from 'eventemitter3';
 import * as merge from 'object-assign-deep';
 import * as uuid from 'uuid';
 
+import initStore from '../store';
 import { hostname, apiUrl, swPublickey, version, lang, googleMapsApiKey } from '../config';
 import Progress from './scripts/loading';
 import Connection from './scripts/streaming/stream';
@@ -16,16 +17,6 @@ import Err from '../common/views/components/connect-failed.vue';
 import { LocalTimelineStreamManager } from './scripts/streaming/local-timeline';
 import { GlobalTimelineStreamManager } from './scripts/streaming/global-timeline';
 
-const defaultSettings = {
-	fetchOnScroll: true,
-	showMaps: true,
-	showPostFormOnTopOfTl: false,
-	gradientWindowHeader: false,
-	showReplyTarget: true,
-	showMyRenotes: true,
-	showRenotedMyNotes: true
-};
-
 //#region api requests
 let spinner = null;
 let pending = 0;
@@ -117,6 +108,8 @@ export default class MiOS extends EventEmitter {
 		return localStorage.getItem('enableSounds') == 'true';
 	}
 
+	public store: ReturnType<typeof initStore>;
+
 	public apis: API;
 
 	/**
@@ -232,6 +225,11 @@ export default class MiOS extends EventEmitter {
 		console.error.apply(null, args);
 	}
 
+	public bakeMe() {
+		// ローカルストレージにキャッシュ
+		localStorage.setItem('me', JSON.stringify(this.i));
+	}
+
 	public signout() {
 		localStorage.removeItem('me');
 		document.cookie = `i=; domain=${hostname}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
@@ -243,6 +241,8 @@ export default class MiOS extends EventEmitter {
 	 * @param callback A function that call when initialized
 	 */
 	public async init(callback) {
+		this.store = initStore(this);
+
 		//#region Init stream managers
 		this.streams.serverStream = new ServerStreamManager(this);
 
@@ -307,16 +307,11 @@ export default class MiOS extends EventEmitter {
 
 		// フェッチが完了したとき
 		const fetched = me => {
-			if (me) {
-				// デフォルトの設定をマージ
-				me.clientSettings = Object.assign(defaultSettings, me.clientSettings);
-
-				// ローカルストレージにキャッシュ
-				localStorage.setItem('me', JSON.stringify(me));
-			}
-
 			this.i = me;
 
+			// ローカルストレージにキャッシュ
+			this.bakeMe();
+
 			this.emit('signedin');
 
 			// Finish init
@@ -333,6 +328,14 @@ export default class MiOS extends EventEmitter {
 		// Get cached account data
 		const cachedMe = JSON.parse(localStorage.getItem('me'));
 
+		//#region キャッシュされた設定を復元
+		const cachedSettings = JSON.parse(localStorage.getItem('settings'));
+
+		if (cachedSettings) {
+			this.store.commit('settings/init', cachedSettings);
+		}
+		//#endregion
+
 		// キャッシュがあったとき
 		if (cachedMe) {
 			if (cachedMe.token == null) {
@@ -346,12 +349,25 @@ export default class MiOS extends EventEmitter {
 			// 後から新鮮なデータをフェッチ
 			fetchme(cachedMe.token, freshData => {
 				merge(cachedMe, freshData);
+
+				this.store.commit('settings/init', freshData.clientSettings);
 			});
 		} else {
 			// Get token from cookie
 			const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1];
 
-			fetchme(i, fetched);
+			fetchme(i, me => {
+				if (me) {
+					Object.entries(me.clientSettings).forEach(([key, value]) => {
+						this.store.commit('settings/set', { key, value });
+					});
+
+					fetched(me);
+				} else {
+					// Finish init
+					callback();
+				}
+			});
 		}
 	}
 
@@ -456,7 +472,7 @@ export default class MiOS extends EventEmitter {
 		};
 
 		const promise = new Promise((resolve, reject) => {
-			const viaStream = this.stream.hasConnection &&
+			const viaStream = this.stream && this.stream.hasConnection &&
 				(localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true);
 
 			if (viaStream) {
diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts
index 73f2c5302c..ddb0d4820e 100644
--- a/src/client/app/common/scripts/streaming/home.ts
+++ b/src/client/app/common/scripts/streaming/home.ts
@@ -25,10 +25,31 @@ export class HomeStream extends Stream {
 				console.log('I updated:', i);
 			}
 			merge(me, i);
+
+			// キャッシュ更新
+			os.bakeMe();
+		});
+
+		this.on('clientSettingUpdated', x => {
+			os.store.commit('settings/set', {
+				key: x.key,
+				value: x.value
+			});
+		});
+
+		this.on('home_updated', x => {
+			if (x.home) {
+				os.store.commit('settings/setHome', x.home);
+			} else {
+				os.store.commit('settings/setHomeWidget', {
+					id: x.id,
+					data: x.data
+				});
+			}
 		});
 
 		// トークンが再生成されたとき
-		// このままではAPIが利用できないので強制的にサインアウトさせる
+		// このままではMisskeyが利用できないので強制的にサインアウトさせる
 		this.on('my_token_regenerated', () => {
 			alert('%i18n:!common.my-token-regenerated%');
 			os.signout();
diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue
new file mode 100644
index 0000000000..5aac9c8ba1
--- /dev/null
+++ b/src/client/app/common/views/components/avatar.vue
@@ -0,0 +1,38 @@
+<template>
+	<router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="{ borderRadius: clientSettings.circleIcons ? '100%' : null }">
+		<img v-if="disablePreview" :src="`${user.avatarUrl}?thumbnail&size=128`" alt=""/>
+		<img v-else :src="`${user.avatarUrl}?thumbnail&size=128`" alt="" v-user-preview="user.id"/>
+	</router-link>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		user: {
+			required: true
+		},
+		target: {
+			required: false,
+			default: null
+		},
+		disablePreview: {
+			required: false,
+			default: false
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-avatar
+	display block
+
+	> img
+		display inline-block
+		width 100%
+		height 100%
+		margin 0
+		border-radius inherit
+		vertical-align bottom
+</style>
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 6bfe43a800..69fed00c74 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -3,6 +3,7 @@ import Vue from 'vue';
 import signin from './signin.vue';
 import signup from './signup.vue';
 import forkit from './forkit.vue';
+import avatar from './avatar.vue';
 import nav from './nav.vue';
 import noteHtml from './note-html';
 import poll from './poll.vue';
@@ -28,6 +29,7 @@ import welcomeTimeline from './welcome-timeline.vue';
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
 Vue.component('mk-forkit', forkit);
+Vue.component('mk-avatar', avatar);
 Vue.component('mk-nav', nav);
 Vue.component('mk-note-html', noteHtml);
 Vue.component('mk-poll', poll);
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 70df899f5a..ba0ab3209f 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="message" :data-is-me="isMe">
-	<router-link class="avatar-anchor" :to="message.user | userPage" :title="message.user | acct" target="_blank">
-		<img class="avatar" :src="`${message.user.avatarUrl}?thumbnail&size=80`" alt=""/>
-	</router-link>
+	<mk-avatar class="avatar" :user="message.user" target="_blank"/>
 	<div class="content">
 		<div class="balloon" :data-no-text="message.text == null">
 			<p class="read" v-if="isMe && message.isRead">%i18n:@is-read%</p>
@@ -67,20 +65,14 @@ export default Vue.extend({
 	padding 10px 12px 10px 12px
 	background-color transparent
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		position absolute
 		top 10px
-
-		> .avatar
-			display block
-			min-width 54px
-			min-height 54px
-			max-width 54px
-			max-height 54px
-			margin 0
-			border-radius 8px
-			transition all 0.1s ease
+		width 54px
+		height 54px
+		border-radius 8px
+		transition all 0.1s ease
 
 	> .content
 
@@ -201,7 +193,7 @@ export default Vue.extend({
 				margin-left 4px
 
 	&:not([data-is-me])
-		> .avatar-anchor
+		> .avatar
 			left 12px
 
 		> .content
@@ -225,7 +217,7 @@ export default Vue.extend({
 				text-align left
 
 	&[data-is-me]
-		> .avatar-anchor
+		> .avatar
 			right 12px
 
 		> .content
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index 6f8fcb3a70..11f9c366d4 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -13,7 +13,7 @@
 					@click="navigate(user)"
 					tabindex="-1"
 				>
-					<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
+					<mk-avatar class="avatar" :user="user"/>
 					<span class="name">{{ user | userName }}</span>
 					<span class="username">@{{ user | acct }}</span>
 				</li>
@@ -31,7 +31,7 @@
 				:key="message.id"
 			>
 				<div>
-					<img class="avatar" :src="`${isMe(message) ? message.recipient.avatarUrl : message.user.avatarUrl}?thumbnail&size=64`" alt=""/>
+					<mk-avatar class="avatar" :user="isMe(message) ? message.recipient : message.user"/>
 					<header>
 						<span class="name">{{ isMe(message) ? message.recipient : message.user | userName }}</span>
 						<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 3497976901..6fadb030c3 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -1,9 +1,7 @@
 <template>
 <div class="mk-welcome-timeline">
 	<div v-for="note in notes">
-		<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.user.id">
-			<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="note.user" target="_blank"/>
 		<div class="body">
 			<header>
 				<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
@@ -69,18 +67,15 @@ export default Vue.extend({
 			display block
 			clear both
 
-		> .avatar-anchor
+		> .avatar
 			display block
 			float left
 			position -webkit-sticky
 			position sticky
 			top 16px
-
-			> img
-				display block
-				width 42px
-				height 42px
-				border-radius 6px
+			width 42px
+			height 42px
+			border-radius 6px
 
 		> .body
 			float right
diff --git a/src/client/app/common/views/widgets/access-log.vue b/src/client/app/common/views/widgets/access-log.vue
index 0b1c7fe2dd..8652e35645 100644
--- a/src/client/app/common/views/widgets/access-log.vue
+++ b/src/client/app/common/views/widgets/access-log.vue
@@ -61,6 +61,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue
index 96d1d0ef3a..75b1d60524 100644
--- a/src/client/app/common/views/widgets/broadcast.vue
+++ b/src/client/app/common/views/widgets/broadcast.vue
@@ -68,6 +68,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue
index 0bb503759c..41e9253784 100644
--- a/src/client/app/common/views/widgets/calendar.vue
+++ b/src/client/app/common/views/widgets/calendar.vue
@@ -73,6 +73,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		},
 		tick() {
 			const now = new Date();
diff --git a/src/client/app/common/views/widgets/photo-stream.vue b/src/client/app/common/views/widgets/photo-stream.vue
index c51d932bd1..ae5924bb10 100644
--- a/src/client/app/common/views/widgets/photo-stream.vue
+++ b/src/client/app/common/views/widgets/photo-stream.vue
@@ -59,6 +59,8 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/common/views/widgets/rss.vue b/src/client/app/common/views/widgets/rss.vue
index f0ba11678e..b5339add0b 100644
--- a/src/client/app/common/views/widgets/rss.vue
+++ b/src/client/app/common/views/widgets/rss.vue
@@ -40,6 +40,7 @@ export default define({
 	methods: {
 		func() {
 			this.props.compact = !this.props.compact;
+			this.save();
 		},
 		fetch() {
 			fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.url}`, {
diff --git a/src/client/app/common/views/widgets/server.vue b/src/client/app/common/views/widgets/server.vue
index 2fbc07adf0..2fdd60499b 100644
--- a/src/client/app/common/views/widgets/server.vue
+++ b/src/client/app/common/views/widgets/server.vue
@@ -68,6 +68,7 @@ export default define({
 			} else {
 				this.props.view++;
 			}
+			this.save();
 		},
 		func() {
 			if (this.props.design == 2) {
@@ -75,6 +76,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/common/views/widgets/slideshow.vue b/src/client/app/common/views/widgets/slideshow.vue
index 95be4b94fd..459b24a32f 100644
--- a/src/client/app/common/views/widgets/slideshow.vue
+++ b/src/client/app/common/views/widgets/slideshow.vue
@@ -64,6 +64,7 @@ export default define({
 			} else {
 				this.props.size++;
 			}
+			this.save();
 
 			this.applySize();
 		},
@@ -111,6 +112,7 @@ export default define({
 		choose() {
 			(this as any).apis.chooseDriveFolder().then(folder => {
 				this.props.folder = folder ? folder.id : null;
+				this.save();
 				this.fetch();
 			});
 		}
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index af5bde3ad5..3c1f8b8257 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -3,9 +3,7 @@
 	<p class="title">気になるユーザーをフォロー:</p>
 	<div class="users" v-if="!fetching && users.length > 0">
 		<div class="user" v-for="user in users" :key="user.id">
-			<router-link class="avatar-anchor" :to="user | userPage">
-				<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="user.id"/>
-			</router-link>
+			<mk-avatar class="avatar" :user="user" target="_blank"/>
 			<div class="body">
 				<router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link>
 				<p class="username">@{{ user | acct }}</p>
@@ -86,18 +84,13 @@ export default Vue.extend({
 				display block
 				clear both
 
-			> .avatar-anchor
+			> .avatar
 				display block
 				float left
 				margin 0 12px 0 0
-
-				> .avatar
-					display block
-					width 42px
-					height 42px
-					margin 0
-					border-radius 8px
-					vertical-align bottom
+				width 42px
+				height 42px
+				border-radius 8px
 
 			> .body
 				float left
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index 4343a7fb78..5337be61fd 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -53,7 +53,7 @@
 			<div class="main">
 				<a @click="hint">カスタマイズのヒント</a>
 				<div>
-					<mk-post-form v-if="os.i.clientSettings.showPostFormOnTopOfTl"/>
+					<mk-post-form v-if="clientSettings.showPostFormOnTopOfTl"/>
 					<mk-timeline ref="tl" @loaded="onTlLoaded"/>
 				</div>
 			</div>
@@ -63,7 +63,7 @@
 				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp"/>
 			</div>
 			<div class="main">
-				<mk-post-form v-if="os.i.clientSettings.showPostFormOnTopOfTl"/>
+				<mk-post-form v-if="clientSettings.showPostFormOnTopOfTl"/>
 				<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
 				<mk-mentions @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
 			</div>
@@ -81,6 +81,7 @@ export default Vue.extend({
 	components: {
 		XDraggable
 	},
+
 	props: {
 		customize: {
 			type: Boolean,
@@ -91,61 +92,43 @@ export default Vue.extend({
 			default: 'timeline'
 		}
 	},
+
 	data() {
 		return {
 			connection: null,
 			connectionId: null,
 			widgetAdderSelected: null,
-			trash: [],
-			widgets: {
-				left: [],
-				right: []
-			}
+			trash: []
 		};
 	},
+
 	computed: {
-		home: {
-			get(): any[] {
-				//#region 互換性のため
-				(this as any).os.i.clientSettings.home.forEach(w => {
-					if (w.name == 'rss-reader') w.name = 'rss';
-					if (w.name == 'user-recommendation') w.name = 'users';
-					if (w.name == 'recommended-polls') w.name = 'polls';
-				});
-				//#endregion
-				return (this as any).os.i.clientSettings.home;
-			},
-			set(value) {
-				(this as any).os.i.clientSettings.home = value;
-			}
+		home(): any[] {
+			return this.$store.state.settings.data.home;
 		},
 		left(): any[] {
 			return this.home.filter(w => w.place == 'left');
 		},
 		right(): any[] {
 			return this.home.filter(w => w.place == 'right');
+		},
+		widgets(): any {
+			return {
+				left: this.left,
+				right: this.right
+			};
 		}
 	},
-	created() {
-		this.widgets.left = this.left;
-		this.widgets.right = this.right;
-		this.$watch('os.i.clientSettings', i => {
-			this.widgets.left = this.left;
-			this.widgets.right = this.right;
-		}, {
-			deep: true
-		});
-	},
+
 	mounted() {
 		this.connection = (this as any).os.stream.getConnection();
 		this.connectionId = (this as any).os.stream.use();
-
-		this.connection.on('home_updated', this.onHomeUpdated);
 	},
+
 	beforeDestroy() {
-		this.connection.off('home_updated', this.onHomeUpdated);
 		(this as any).os.stream.dispose(this.connectionId);
 	},
+
 	methods: {
 		hint() {
 			(this as any).apis.dialog({
@@ -159,56 +142,44 @@ export default Vue.extend({
 				}]
 			});
 		},
+
 		onTlLoaded() {
 			this.$emit('loaded');
 		},
-		onHomeUpdated(data) {
-			if (data.home) {
-				(this as any).os.i.clientSettings.home = data.home;
-				this.widgets.left = data.home.filter(w => w.place == 'left');
-				this.widgets.right = data.home.filter(w => w.place == 'right');
-			} else {
-				const w = (this as any).os.i.clientSettings.home.find(w => w.id == data.id);
-				if (w != null) {
-					w.data = data.data;
-					this.$refs[w.id][0].preventSave = true;
-					this.$refs[w.id][0].props = w.data;
-					this.widgets.left = (this as any).os.i.clientSettings.home.filter(w => w.place == 'left');
-					this.widgets.right = (this as any).os.i.clientSettings.home.filter(w => w.place == 'right');
-				}
-			}
-		},
+
 		onWidgetContextmenu(widgetId) {
 			const w = (this.$refs[widgetId] as any)[0];
 			if (w.func) w.func();
 		},
+
 		onWidgetSort() {
 			this.saveHome();
 		},
+
 		onTrash(evt) {
 			this.saveHome();
 		},
+
 		addWidget() {
-			const widget = {
+			this.$store.dispatch('settings/addHomeWidget', {
 				name: this.widgetAdderSelected,
 				id: uuid(),
 				place: 'left',
 				data: {}
-			};
-
-			this.widgets.left.unshift(widget);
-			this.saveHome();
+			});
 		},
+
 		saveHome() {
 			const left = this.widgets.left;
 			const right = this.widgets.right;
-			this.home = left.concat(right);
+			this.$store.commit('settings/setHome', left.concat(right));
 			left.forEach(w => w.place = 'left');
 			right.forEach(w => w.place = 'right');
 			(this as any).api('i/update_home', {
 				home: this.home
 			});
 		},
+
 		warp(date) {
 			(this.$refs.tl as any).warp(date);
 		}
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 5175c8bd45..24550c4e94 100644
--- a/src/client/app/desktop/views/components/note-detail.sub.vue
+++ b/src/client/app/desktop/views/components/note-detail.sub.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="sub" :title="title">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="note.user"/>
 	<div class="main">
 		<header>
 			<div class="left">
@@ -57,18 +55,13 @@ root(isDark)
 		> .main > footer > button
 			color #888
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 16px 0 0
-
-		> .avatar
-			display block
-			width 44px
-			height 44px
-			margin 0
-			border-radius 4px
-			vertical-align bottom
+		width 44px
+		height 44px
+		border-radius 4px
 
 	> .main
 		float left
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index 525023349e..5d07dc90d4 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -18,18 +18,14 @@
 	</div>
 	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
-				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
-			</router-link>
+			<mk-avatar class="avatar" :user="note.user"/>
 			%fa:retweet%
 			<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
 			がRenote
 		</p>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="p.user | userPage">
-			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="p.user"/>
 		<header>
 			<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
 			<span class="username">@{{ p.user | acct }}</span>
@@ -159,7 +155,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
@@ -262,17 +258,12 @@ root(isDark)
 			margin 0
 			padding 16px 32px
 
-			.avatar-anchor
+			.avatar
 				display inline-block
-
-				.avatar
-					vertical-align bottom
-					min-width 28px
-					min-height 28px
-					max-width 28px
-					max-height 28px
-					margin 0 8px 0 0
-					border-radius 6px
+				width 28px
+				height 28px
+				margin 0 8px 0 0
+				border-radius 6px
 
 			[data-fa]
 				margin-right 4px
@@ -298,18 +289,10 @@ root(isDark)
 			> footer > button
 				color isDark ? #707b97 : #888
 
-		> .avatar-anchor
-			display block
+		> .avatar
 			width 60px
 			height 60px
-
-			> .avatar
-				display block
-				width 60px
-				height 60px
-				margin 0
-				border-radius 8px
-				vertical-align bottom
+			border-radius 8px
 
 		> header
 			position absolute
diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue
index b45814e512..43eb159885 100644
--- a/src/client/app/desktop/views/components/note-preview.vue
+++ b/src/client/app/desktop/views/components/note-preview.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="mk-note-preview" :title="title">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="note.user"/>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
@@ -41,18 +39,13 @@ root(isDark)
 		display block
 		clear both
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 16px 0 0
-
-		> .avatar
-			display block
-			width 52px
-			height 52px
-			margin 0
-			border-radius 8px
-			vertical-align bottom
+		width 52px
+		height 52px
+		border-radius 8px
 
 	> .main
 		float left
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 4472ddefb8..238fb03691 100644
--- a/src/client/app/desktop/views/components/notes.note.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="sub" :title="title">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="note.user"/>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
@@ -53,18 +51,13 @@ root(isDark)
 		display block
 		clear both
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 14px 0 0
-
-		> .avatar
-			display block
-			width 52px
-			height 52px
-			margin 0
-			border-radius 8px
-			vertical-align bottom
+		width 52px
+		height 52px
+		border-radius 8px
 
 	> .main
 		float left
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index ee24543ebf..b512f78eca 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -1,12 +1,10 @@
 <template>
 <div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
-	<div class="reply-to" v-if="p.reply && (!os.isSignedIn || os.i.clientSettings.showReplyTarget)">
+	<div class="reply-to" v-if="p.reply && (!os.isSignedIn || clientSettings.showReplyTarget)">
 		<x-sub :note="p.reply"/>
 	</div>
 	<div class="renote" v-if="isRenote">
-		<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
-			<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="note.user"/>
 		%fa:retweet%
 		<span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
 		<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
@@ -14,9 +12,7 @@
 		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="p.user | userPage">
-			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="p.user"/>
 		<div class="main">
 			<header>
 				<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
@@ -182,7 +178,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
@@ -343,15 +339,12 @@ root(isDark)
 		color #9dbb00
 		background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
 
-		.avatar-anchor
+		.avatar
 			display inline-block
-
-			.avatar
-				vertical-align bottom
-				width 28px
-				height 28px
-				margin 0 8px 0 0
-				border-radius 6px
+			width 28px
+			height 28px
+			margin 0 8px 0 0
+			border-radius 6px
 
 		[data-fa]
 			margin-right 4px
@@ -390,22 +383,17 @@ root(isDark)
 			> .main > footer > button
 				color isDark ? #707b97 : #888
 
-		> .avatar-anchor
+		> .avatar
 			display block
 			float left
 			margin 0 16px 10px 0
+			width 58px
+			height 58px
+			border-radius 8px
 			//position -webkit-sticky
 			//position sticky
 			//top 74px
 
-			> .avatar
-				display block
-				width 58px
-				height 58px
-				margin 0
-				border-radius 8px
-				vertical-align bottom
-
 		> .main
 			float left
 			width calc(100% - 74px)
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index c4824feeac..7e80e6f74a 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -121,13 +121,13 @@ export default Vue.extend({
 			const isMyNote = note.userId == (this as any).os.i.id;
 			const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
 
-			if ((this as any).os.i.clientSettings.showMyRenotes === false) {
+			if ((this as any).clientSettings.showMyRenotes === false) {
 				if (isMyNote && isPureRenote) {
 					return;
 				}
 			}
 
-			if ((this as any).os.i.clientSettings.showRenotedMyNotes === false) {
+			if ((this as any).clientSettings.showRenotedMyNotes === false) {
 				if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) {
 					return;
 				}
@@ -199,7 +199,7 @@ export default Vue.extend({
 				this.clearNotification();
 			}
 
-			if ((this as any).os.i.clientSettings.fetchOnScroll !== false) {
+			if ((this as any).clientSettings.fetchOnScroll !== false) {
 				const current = window.scrollY + window.innerHeight;
 				if (current > document.body.offsetHeight - 8) this.loadMore();
 			}
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 36e9dce6af..7923d1a62d 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -6,9 +6,7 @@
 				<div class="notification" :class="notification.type" :key="notification.id">
 					<mk-time :time="notification.createdAt"/>
 					<template v-if="notification.type == 'reaction'">
-						<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
-							<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :user="notification.user"/>
 						<div class="text">
 							<p>
 								<mk-reaction-icon :reaction="notification.reaction"/>
@@ -20,9 +18,7 @@
 						</div>
 					</template>
 					<template v-if="notification.type == 'renote'">
-						<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
-							<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :user="notification.note.user"/>
 						<div class="text">
 							<p>%fa:retweet%
 								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
@@ -33,9 +29,7 @@
 						</div>
 					</template>
 					<template v-if="notification.type == 'quote'">
-						<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
-							<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :user="notification.note.user"/>
 						<div class="text">
 							<p>%fa:quote-left%
 								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
@@ -44,9 +38,7 @@
 						</div>
 					</template>
 					<template v-if="notification.type == 'follow'">
-						<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
-							<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :user="notification.user"/>
 						<div class="text">
 							<p>%fa:user-plus%
 								<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
@@ -54,9 +46,7 @@
 						</div>
 					</template>
 					<template v-if="notification.type == 'reply'">
-						<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
-							<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :user="notification.note.user"/>
 						<div class="text">
 							<p>%fa:reply%
 								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
@@ -65,9 +55,7 @@
 						</div>
 					</template>
 					<template v-if="notification.type == 'mention'">
-						<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
-							<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :user="notification.note.user"/>
 						<div class="text">
 							<p>%fa:at%
 								<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
@@ -76,9 +64,7 @@
 						</div>
 					</template>
 					<template v-if="notification.type == 'poll_vote'">
-						<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
-							<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :user="notification.user"/>
 						<div class="text">
 							<p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p>
 							<router-link class="note-ref" :to="notification.note | notePage">
@@ -223,20 +209,15 @@ root(isDark)
 					display block
 					clear both
 
-				> .avatar-anchor
+				> .avatar
 					display block
 					float left
 					position -webkit-sticky
 					position sticky
 					top 16px
-
-					> img
-						display block
-						min-width 36px
-						min-height 36px
-						max-width 36px
-						max-height 36px
-						border-radius 6px
+					width 36px
+					height 36px
+					border-radius 6px
 
 				> .text
 					float right
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 9d56042ea7..af6ab12669 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -20,7 +20,7 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>動作</h1>
-			<mk-switch v-model="os.i.clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
+			<mk-switch v-model="clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
 				<span>ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。</span>
 			</mk-switch>
 			<mk-switch v-model="autoPopout" text="ウィンドウの自動ポップアウト">
@@ -41,13 +41,14 @@
 			</div>
 			<div class="div">
 				<mk-switch v-model="darkmode" text="ダークモード"/>
-				<mk-switch v-model="os.i.clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
+				<mk-switch v-model="clientSettings.circleIcons" @change="onChangeCircleIcons" text="丸いアイコンを使用"/>
+				<mk-switch v-model="clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
 			</div>
-			<mk-switch v-model="os.i.clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
-			<mk-switch v-model="os.i.clientSettings.showReplyTarget" @change="onChangeShowReplyTarget" text="リプライ先を表示する"/>
-			<mk-switch v-model="os.i.clientSettings.showMyRenotes" @change="onChangeShowMyRenotes" text="自分の行ったRenoteをタイムラインに表示する"/>
-			<mk-switch v-model="os.i.clientSettings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="Renoteされた自分の投稿をタイムラインに表示する"/>
-			<mk-switch v-model="os.i.clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
+			<mk-switch v-model="clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
+			<mk-switch v-model="clientSettings.showReplyTarget" @change="onChangeShowReplyTarget" text="リプライ先を表示する"/>
+			<mk-switch v-model="clientSettings.showMyRenotes" @change="onChangeShowMyRenotes" text="自分の行ったRenoteをタイムラインに表示する"/>
+			<mk-switch v-model="clientSettings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="Renoteされた自分の投稿をタイムラインに表示する"/>
+			<mk-switch v-model="clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
 				<span>位置情報が添付された投稿のマップを自動的に展開します。</span>
 			</mk-switch>
 		</section>
@@ -69,7 +70,7 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>モバイル</h1>
-			<mk-switch v-model="os.i.clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
+			<mk-switch v-model="clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -297,8 +298,8 @@ export default Vue.extend({
 			this.$emit('done');
 		},
 		onChangeFetchOnScroll(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'fetchOnScroll',
+			this.$store.dispatch('settings/set', {
+				key: 'fetchOnScroll',
 				value: v
 			});
 		},
@@ -308,50 +309,56 @@ export default Vue.extend({
 			});
 		},
 		onChangeDark(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'dark',
+			this.$store.dispatch('settings/set', {
+				key: 'dark',
 				value: v
 			});
 		},
 		onChangeShowPostFormOnTopOfTl(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'showPostFormOnTopOfTl',
+			this.$store.dispatch('settings/set', {
+				key: 'showPostFormOnTopOfTl',
 				value: v
 			});
 		},
 		onChangeShowReplyTarget(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'showReplyTarget',
+			this.$store.dispatch('settings/set', {
+				key: 'showReplyTarget',
 				value: v
 			});
 		},
 		onChangeShowMyRenotes(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'showMyRenotes',
+			this.$store.dispatch('settings/set', {
+				key: 'showMyRenotes',
 				value: v
 			});
 		},
 		onChangeShowRenotedMyNotes(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'showRenotedMyNotes',
+			this.$store.dispatch('settings/set', {
+				key: 'showRenotedMyNotes',
 				value: v
 			});
 		},
 		onChangeShowMaps(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'showMaps',
+			this.$store.dispatch('settings/set', {
+				key: 'showMaps',
+				value: v
+			});
+		},
+		onChangeCircleIcons(v) {
+			this.$store.dispatch('settings/set', {
+				key: 'circleIcons',
 				value: v
 			});
 		},
 		onChangeGradientWindowHeader(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'gradientWindowHeader',
+			this.$store.dispatch('settings/set', {
+				key: 'gradientWindowHeader',
 				value: v
 			});
 		},
 		onChangeDisableViaMobile(v) {
-			(this as any).api('i/update_client_setting', {
-				name: 'disableViaMobile',
+			this.$store.dispatch('settings/set', {
+				key: 'disableViaMobile',
 				value: v
 			});
 		},
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue
index a137a57070..254a5b9d63 100644
--- a/src/client/app/desktop/views/components/timeline.core.vue
+++ b/src/client/app/desktop/views/components/timeline.core.vue
@@ -101,8 +101,8 @@ export default Vue.extend({
 				(this as any).api(this.endpoint, {
 					limit: fetchLimit + 1,
 					untilDate: this.date ? this.date.getTime() : undefined,
-					includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-					includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+					includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+					includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 				}).then(notes => {
 					if (notes.length == fetchLimit + 1) {
 						notes.pop();
@@ -123,8 +123,8 @@ export default Vue.extend({
 			(this as any).api(this.endpoint, {
 				limit: fetchLimit + 1,
 				untilId: (this.$refs.timeline as any).tail().id,
-				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+				includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+				includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 			}).then(notes => {
 				if (notes.length == fetchLimit + 1) {
 					notes.pop();
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 145666b4bc..897d3de816 100644
--- a/src/client/app/desktop/views/components/ui.header.account.vue
+++ b/src/client/app/desktop/views/components/ui.header.account.vue
@@ -2,7 +2,7 @@
 <div class="account">
 	<button class="header" :data-active="isOpen" @click="toggle">
 		<span class="username">{{ os.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
-		<img class="avatar" :src="`${ os.i.avatarUrl }?thumbnail&size=64`" alt="avatar"/>
+		<mk-avatar class="avatar" :user="os.i"/>
 	</button>
 	<transition name="zoom-in-top">
 		<div class="menu" v-if="isOpen">
diff --git a/src/client/app/desktop/views/components/user-list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue
index ee983a969c..ccdf8f64d2 100644
--- a/src/client/app/desktop/views/components/user-list-timeline.vue
+++ b/src/client/app/desktop/views/components/user-list-timeline.vue
@@ -46,8 +46,8 @@ export default Vue.extend({
 				(this as any).api('notes/user-list-timeline', {
 					listId: this.list.id,
 					limit: fetchLimit + 1,
-					includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-					includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+					includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+					includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 				}).then(notes => {
 					if (notes.length == fetchLimit + 1) {
 						notes.pop();
@@ -66,8 +66,8 @@ export default Vue.extend({
 				listId: this.list.id,
 				limit: fetchLimit + 1,
 				untilId: (this.$refs.timeline as any).tail().id,
-				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+				includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+				includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 			}).then(notes => {
 				if (notes.length == fetchLimit + 1) {
 					notes.pop();
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index d0111d7dc5..cc5e021390 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -2,9 +2,7 @@
 <div class="mk-user-preview">
 	<template v-if="u != null">
 		<div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl}?thumbnail&size=512)` : ''"></div>
-		<router-link class="avatar" :to="u | userPage">
-			<img :src="`${u.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="u" :disable-preview="true"/>
 		<div class="title">
 			<router-link class="name" :to="u | userPage">{{ u | userName }}</router-link>
 			<p class="username">@{{ u | acct }}</p>
@@ -111,14 +109,10 @@ root(isDark)
 		top 62px
 		left 13px
 		z-index 2
-
-		> img
-			display block
-			width 58px
-			height 58px
-			margin 0
-			border solid 3px isDark ? #282c37 : #fff
-			border-radius 8px
+		width 58px
+		height 58px
+		border solid 3px isDark ? #282c37 : #fff
+		border-radius 8px
 
 	> .title
 		display block
diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue
index 005c9cd6d3..dbad295178 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="root item">
-	<router-link class="avatar-anchor" :to="user | userPage" v-user-preview="user.id">
-		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="user"/>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link>
@@ -35,18 +33,13 @@ export default Vue.extend({
 		display block
 		clear both
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 16px 0 0
-
-		> .avatar
-			display block
-			width 58px
-			height 58px
-			margin 0
-			border-radius 8px
-			vertical-align bottom
+		width 58px
+		height 58px
+		border-radius 8px
 
 	> .main
 		float left
diff --git a/src/client/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue
index 2edba5a230..ab8327d39e 100644
--- a/src/client/app/desktop/views/components/widget-container.vue
+++ b/src/client/app/desktop/views/components/widget-container.vue
@@ -24,8 +24,8 @@ export default Vue.extend({
 	computed: {
 		withGradient(): boolean {
 			return (this as any).os.isSignedIn
-				? (this as any).os.i.clientSettings.gradientWindowHeader != null
-					? (this as any).os.i.clientSettings.gradientWindowHeader
+				? (this as any).clientSettings.gradientWindowHeader != null
+					? (this as any).clientSettings.gradientWindowHeader
 					: false
 				: false;
 		}
diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
index c9820f8697..91d1a9c2bf 100644
--- a/src/client/app/desktop/views/components/window.vue
+++ b/src/client/app/desktop/views/components/window.vue
@@ -94,8 +94,8 @@ export default Vue.extend({
 		},
 		withGradient(): boolean {
 			return (this as any).os.isSignedIn
-				? (this as any).os.i.clientSettings.gradientWindowHeader != null
-					? (this as any).os.i.clientSettings.gradientWindowHeader
+				? (this as any).clientSettings.gradientWindowHeader != null
+					? (this as any).clientSettings.gradientWindowHeader
 					: false
 				: false;
 		}
diff --git a/src/client/app/desktop/views/pages/user-list.users.vue b/src/client/app/desktop/views/pages/user-list.users.vue
index 63070ed609..4236cdbb14 100644
--- a/src/client/app/desktop/views/pages/user-list.users.vue
+++ b/src/client/app/desktop/views/pages/user-list.users.vue
@@ -8,9 +8,7 @@
 			<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw% %i18n:common.loading%<mk-ellipsis/></p>
 			<template v-else-if="users.length != 0">
 				<div class="user" v-for="_user in users">
-					<router-link class="avatar-anchor" :to="_user | userPage">
-						<img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
-					</router-link>
+					<mk-avatar class="avatar" :user="_user"/>
 					<div class="body">
 						<router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link>
 						<p class="username">@{{ _user | acct }}</p>
@@ -80,18 +78,13 @@ root(isDark)
 			display block
 			clear both
 
-		> .avatar-anchor
+		> .avatar
 			display block
 			float left
 			margin 0 12px 0 0
-
-			> .avatar
-				display block
-				width 42px
-				height 42px
-				margin 0
-				border-radius 8px
-				vertical-align bottom
+			width 42px
+			height 42px
+			border-radius 8px
 
 		> .body
 			float left
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 161b08d1de..4af0f0bca6 100644
--- a/src/client/app/desktop/views/pages/user/user.friends.vue
+++ b/src/client/app/desktop/views/pages/user/user.friends.vue
@@ -4,9 +4,7 @@
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
 	<template v-if="!fetching && users.length != 0">
 		<div class="user" v-for="friend in users">
-			<router-link class="avatar-anchor" :to="friend | userPage">
-				<img class="avatar" :src="`${friend.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/>
-			</router-link>
+			<mk-avatar class="avatar" :user="friend"/>
 			<div class="body">
 				<router-link class="name" :to="friend | userPage" v-user-preview="friend.id">{{ friend.name }}</router-link>
 				<p class="username">@{{ friend | acct }}</p>
@@ -82,18 +80,13 @@ export default Vue.extend({
 			display block
 			clear both
 
-		> .avatar-anchor
+		> .avatar
 			display block
 			float left
 			margin 0 12px 0 0
-
-			> .avatar
-				display block
-				width 42px
-				height 42px
-				margin 0
-				border-radius 8px
-				vertical-align bottom
+			width 42px
+			height 42px
+			border-radius 8px
 
 		> .body
 			float left
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 99fe0b18d0..629f6d87cf 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -7,7 +7,7 @@
 		<div class="fade"></div>
 	</div>
 	<div class="container">
-		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/>
+		<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
 		<div class="title">
 			<p class="name">{{ user | userName }}</p>
 			<p class="username">@{{ user | acct }}</p>
@@ -139,7 +139,6 @@ export default Vue.extend({
 			z-index 2
 			width 160px
 			height 160px
-			margin 0
 			border solid 3px #fff
 			border-radius 8px
 			box-shadow 1px 1px 3px rgba(#000, 0.2)
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 223be8301b..3d6765c97e 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -8,9 +8,7 @@
 					<p>ようこそ！ <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p>
 					<p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
 					<div class="users">
-						<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="user | userPage" v-user-preview="user.id">
-							<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-						</router-link>
+						<mk-avatar class="avatar" :key="user.id" :user="user"/>
 					</div>
 				</div>
 				<div>
diff --git a/src/client/app/desktop/views/widgets/activity.vue b/src/client/app/desktop/views/widgets/activity.vue
index 0bdf4622af..1be87f590c 100644
--- a/src/client/app/desktop/views/widgets/activity.vue
+++ b/src/client/app/desktop/views/widgets/activity.vue
@@ -22,9 +22,11 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		},
 		viewChanged(view) {
 			this.props.view = view;
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/widgets/channel.vue b/src/client/app/desktop/views/widgets/channel.vue
index 600cdd5318..d21aed40fd 100644
--- a/src/client/app/desktop/views/widgets/channel.vue
+++ b/src/client/app/desktop/views/widgets/channel.vue
@@ -37,6 +37,7 @@ export default define({
 	methods: {
 		func() {
 			this.props.compact = !this.props.compact;
+			this.save();
 		},
 		settings() {
 			const id = window.prompt('チャンネルID');
diff --git a/src/client/app/desktop/views/widgets/messaging.vue b/src/client/app/desktop/views/widgets/messaging.vue
index b71db3e087..791d2ff1bb 100644
--- a/src/client/app/desktop/views/widgets/messaging.vue
+++ b/src/client/app/desktop/views/widgets/messaging.vue
@@ -35,6 +35,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/widgets/notifications.vue b/src/client/app/desktop/views/widgets/notifications.vue
index 091b0d8b98..f75a091480 100644
--- a/src/client/app/desktop/views/widgets/notifications.vue
+++ b/src/client/app/desktop/views/widgets/notifications.vue
@@ -23,6 +23,7 @@ export default define({
 		},
 		func() {
 			this.props.compact = !this.props.compact;
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
index 852bf2e3a1..36fcc20636 100644
--- a/src/client/app/desktop/views/widgets/polls.vue
+++ b/src/client/app/desktop/views/widgets/polls.vue
@@ -39,6 +39,7 @@ export default define({
 	methods: {
 		func() {
 			this.props.compact = !this.props.compact;
+			this.save();
 		},
 		fetch() {
 			this.fetching = true;
diff --git a/src/client/app/desktop/views/widgets/post-form.vue b/src/client/app/desktop/views/widgets/post-form.vue
index 9c2d60f9b9..69b21ad37a 100644
--- a/src/client/app/desktop/views/widgets/post-form.vue
+++ b/src/client/app/desktop/views/widgets/post-form.vue
@@ -29,6 +29,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		},
 		onKeydown(e) {
 			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
index 7958b4829a..48e7747b40 100644
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ b/src/client/app/desktop/views/widgets/profile.vue
@@ -36,6 +36,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/widgets/timemachine.vue b/src/client/app/desktop/views/widgets/timemachine.vue
index 6db3b14c62..22a4120403 100644
--- a/src/client/app/desktop/views/widgets/timemachine.vue
+++ b/src/client/app/desktop/views/widgets/timemachine.vue
@@ -22,6 +22,7 @@ export default define({
 			} else {
 				this.props.design++;
 			}
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue
index 08fa7f8027..c33bf2f2f2 100644
--- a/src/client/app/desktop/views/widgets/trends.vue
+++ b/src/client/app/desktop/views/widgets/trends.vue
@@ -38,6 +38,7 @@ export default define({
 	methods: {
 		func() {
 			this.props.compact = !this.props.compact;
+			this.save();
 		},
 		fetch() {
 			this.fetching = true;
diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue
index 6e326115b0..328fa56697 100644
--- a/src/client/app/desktop/views/widgets/users.vue
+++ b/src/client/app/desktop/views/widgets/users.vue
@@ -8,9 +8,7 @@
 			<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 			<template v-else-if="users.length != 0">
 				<div class="user" v-for="_user in users">
-					<router-link class="avatar-anchor" :to="_user | userPage">
-						<img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
-					</router-link>
+					<mk-avatar class="avatar" :user="_user"/>
 					<div class="body">
 						<router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link>
 						<p class="username">@{{ _user | acct }}</p>
@@ -48,6 +46,7 @@ export default define({
 	methods: {
 		func() {
 			this.props.compact = !this.props.compact;
+			this.save();
 		},
 		fetch() {
 			this.fetching = true;
@@ -88,18 +87,13 @@ root(isDark)
 				display block
 				clear both
 
-			> .avatar-anchor
+			> .avatar
 				display block
 				float left
 				margin 0 12px 0 0
-
-				> .avatar
-					display block
-					width 42px
-					height 42px
-					margin 0
-					border-radius 8px
-					vertical-align bottom
+				width 42px
+				height 42px
+				border-radius 8px
 
 			> .body
 				float left
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
index 26f5328d7d..6f7ce02607 100644
--- a/src/client/app/init.ts
+++ b/src/client/app/init.ts
@@ -3,7 +3,7 @@
  */
 
 import Vue from 'vue';
-import Vuex from 'vuex';
+import Vuex, { mapState } from 'vuex';
 import VueRouter from 'vue-router';
 import VModal from 'vue-js-modal';
 import * as TreeView from 'vue-json-tree-view';
@@ -41,17 +41,6 @@ require('./common/views/widgets');
 // Register global filters
 require('./common/views/filters');
 
-const store = new Vuex.Store({
-	state: {
-		uiHeaderHeight: 0
-	},
-	mutations: {
-		setUiHeaderHeight(state, height) {
-			state.uiHeaderHeight = height;
-		}
-	}
-});
-
 Vue.mixin({
 	destroyed(this: any) {
 		if (this.$el.parentNode) {
@@ -159,20 +148,15 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
 						api: os.api,
 						apis: os.apis
 					};
-				}
+				},
+				computed: mapState({
+					clientSettings: state => state.settings.data
+				})
 			});
 
 			const app = new Vue({
-				store,
+				store: os.store,
 				router,
-				created() {
-					this.$watch('os.i', i => {
-						// キャッシュ更新
-						localStorage.setItem('me', JSON.stringify(i));
-					}, {
-						deep: true
-					});
-				},
 				render: createEl => createEl(App)
 			});
 
diff --git a/src/client/app/mobile/views/components/note-detail.sub.vue b/src/client/app/mobile/views/components/note-detail.sub.vue
index 683b5ffd1b..34ff06db30 100644
--- a/src/client/app/mobile/views/components/note-detail.sub.vue
+++ b/src/client/app/mobile/views/components/note-detail.sub.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="root sub">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="note.user"/>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
@@ -43,18 +41,13 @@ root(isDark)
 		display block
 		clear both
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 12px 0 0
-
-		> .avatar
-			display block
-			width 48px
-			height 48px
-			margin 0
-			border-radius 8px
-			vertical-align bottom
+		width 48px
+		height 48px
+		border-radius 8px
 
 	> .main
 		float left
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index 36c3f30c62..b9bd9fe495 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -17,17 +17,12 @@
 	</div>
 	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="note.user | userPage">
-				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
-			</router-link>
-			%fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote
+			<mk-avatar class="avatar" :user="note.user"/>%fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote
 		</p>
 	</div>
 	<article>
 		<header>
-			<router-link class="avatar-anchor" :to="p.user | userPage">
-				<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-			</router-link>
+			<mk-avatar class="avatar" :user="p.user"/>
 			<div>
 				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
 				<span class="username">@{{ p.user | acct }}</span>
@@ -152,7 +147,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
@@ -262,17 +257,12 @@ root(isDark)
 			margin 0
 			padding 16px 32px
 
-			.avatar-anchor
+			.avatar
 				display inline-block
-
-				.avatar
-					vertical-align bottom
-					min-width 28px
-					min-height 28px
-					max-width 28px
-					max-height 28px
-					margin 0 8px 0 0
-					border-radius 6px
+				width 28px
+				height 28px
+				margin 0 8px 0 0
+				border-radius 6px
 
 			[data-fa]
 				margin-right 4px
@@ -301,21 +291,16 @@ root(isDark)
 			display flex
 			line-height 1.1em
 
-			> .avatar-anchor
+			> .avatar
 				display block
-				padding 0 12px 0 0
+				margin 0 12px 0 0
+				width 54px
+				height 54px
+				border-radius 8px
 
-				> .avatar
-					display block
-					width 54px
-					height 54px
-					margin 0
-					border-radius 8px
-					vertical-align bottom
-
-					@media (min-width 500px)
-						width 60px
-						height 60px
+				@media (min-width 500px)
+					width 60px
+					height 60px
 
 			> div
 
diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
index 41244cc759..e9fe9b1bdd 100644
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="mk-note-preview">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="note.user"/>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
@@ -37,18 +35,13 @@ root(isDark)
 		display block
 		clear both
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 12px 0 0
-
-		> .avatar
-			display block
-			width 48px
-			height 48px
-			margin 0
-			border-radius 8px
-			vertical-align bottom
+		width 48px
+		height 48px
+		border-radius 8px
 
 	> .main
 		float left
diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue
index 01f02bdb56..251b377fb2 100644
--- a/src/client/app/mobile/views/components/note.sub.vue
+++ b/src/client/app/mobile/views/components/note.sub.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="sub">
-	<router-link class="avatar-anchor" :to="note.user | userPage">
-		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="note.user"/>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
@@ -49,25 +47,18 @@ root(isDark)
 		display block
 		clear both
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 10px 0 0
+		width 44px
+		height 44px
+		border-radius 8px
 
 		@media (min-width 500px)
 			margin-right 16px
-
-		> .avatar
-			display block
-			width 44px
-			height 44px
-			margin 0
-			border-radius 8px
-			vertical-align bottom
-
-			@media (min-width 500px)
-				width 52px
-				height 52px
+			width 52px
+			height 52px
 
 	> .main
 		float left
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index 07e18544dd..f3aab49bb0 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -1,12 +1,10 @@
 <template>
 <div class="note" :class="{ renote: isRenote }">
-	<div class="reply-to" v-if="p.reply && (!os.isSignedIn || os.i.clientSettings.showReplyTarget)">
+	<div class="reply-to" v-if="p.reply && (!os.isSignedIn || clientSettings.showReplyTarget)">
 		<x-sub :note="p.reply"/>
 	</div>
 	<div class="renote" v-if="isRenote">
-		<router-link class="avatar-anchor" :to="note.user | userPage">
-			<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="note.user"/>
 		%fa:retweet%
 		<span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
 		<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
@@ -14,9 +12,7 @@
 		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="p.user | userPage">
-			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="p.user"/>
 		<div class="main">
 			<header>
 				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
@@ -154,7 +150,7 @@ export default Vue.extend({
 
 		// Draw map
 		if (this.p.geo) {
-			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
 			if (shouldShowMap) {
 				(this as any).os.getGoogleMaps().then(maps => {
 					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
@@ -268,15 +264,12 @@ root(isDark)
 		@media (min-width 600px)
 			padding 16px 32px
 
-		.avatar-anchor
+		.avatar
 			display inline-block
-
-			.avatar
-				vertical-align bottom
-				width 28px
-				height 28px
-				margin 0 8px 0 0
-				border-radius 6px
+			width 28px
+			height 28px
+			margin 0 8px 0 0
+			border-radius 6px
 
 		[data-fa]
 			margin-right 4px
@@ -314,29 +307,22 @@ root(isDark)
 			display block
 			clear both
 
-		> .avatar-anchor
+		> .avatar
 			display block
 			float left
 			margin 0 10px 8px 0
+			width 48px
+			height 48px
+			border-radius 6px
 			//position -webkit-sticky
 			//position sticky
 			//top 62px
 
 			@media (min-width 500px)
 				margin-right 16px
-
-			> .avatar
-				display block
-				width 48px
-				height 48px
-				margin 0
-				border-radius 6px
-				vertical-align bottom
-
-				@media (min-width 500px)
-					width 58px
-					height 58px
-					border-radius 8px
+				width 58px
+				height 58px
+				border-radius 8px
 
 		> .main
 			float left
diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue
index e12dc1d040..9461fe6fe9 100644
--- a/src/client/app/mobile/views/components/notes.vue
+++ b/src/client/app/mobile/views/components/notes.vue
@@ -116,13 +116,13 @@ export default Vue.extend({
 			const isMyNote = note.userId == (this as any).os.i.id;
 			const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
 
-			if ((this as any).os.i.clientSettings.showMyRenotes === false) {
+			if ((this as any).clientSettings.showMyRenotes === false) {
 				if (isMyNote && isPureRenote) {
 					return;
 				}
 			}
 
-			if ((this as any).os.i.clientSettings.showRenotedMyNotes === false) {
+			if ((this as any).clientSettings.showRenotedMyNotes === false) {
 				if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) {
 					return;
 				}
@@ -187,7 +187,7 @@ export default Vue.extend({
 				this.clearNotification();
 			}
 
-			if ((this as any).os.i.clientSettings.fetchOnScroll !== false) {
+			if ((this as any).clientSettings.fetchOnScroll !== false) {
 				const current = window.scrollY + window.innerHeight;
 				if (current > document.body.offsetHeight - 8) this.loadMore();
 			}
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index a4e6b027ed..13ca95075c 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -1,9 +1,7 @@
 <template>
 <div class="mk-notification">
 	<div class="notification reaction" v-if="notification.type == 'reaction'">
-		<router-link class="avatar-anchor" :to="notification.user | userPage">
-			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="notification.user"/>
 		<div>
 			<header>
 				<mk-reaction-icon :reaction="notification.reaction"/>
@@ -18,9 +16,7 @@
 	</div>
 
 	<div class="notification renote" v-if="notification.type == 'renote'">
-		<router-link class="avatar-anchor" :to="notification.user | userPage">
-			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="notification.user"/>
 		<div>
 			<header>
 				%fa:retweet%
@@ -34,9 +30,7 @@
 	</div>
 
 	<div class="notification follow" v-if="notification.type == 'follow'">
-		<router-link class="avatar-anchor" :to="notification.user | userPage">
-			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="notification.user"/>
 		<div>
 			<header>
 				%fa:user-plus%
@@ -47,9 +41,7 @@
 	</div>
 
 	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
-		<router-link class="avatar-anchor" :to="notification.user | userPage">
-			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-		</router-link>
+		<mk-avatar class="avatar" :user="notification.user"/>
 		<div>
 			<header>
 				%fa:chart-pie%
@@ -111,18 +103,16 @@ root(isDark)
 			display block
 			clear both
 
-		> .avatar-anchor
+		> .avatar
 			display block
 			float left
+			width 36px
+			height 36px
+			border-radius 6px
 
-			img
-				width 36px
-				height 36px
-				border-radius 6px
-
-				@media (min-width 500px)
-					width 42px
-					height 42px
+			@media (min-width 500px)
+				width 42px
+				height 42px
 
 		> div
 			float right
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index ec16119797..3f890223a3 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -166,7 +166,7 @@ export default Vue.extend({
 
 		post() {
 			this.posting = true;
-			const viaMobile = (this as any).os.i.clientSettings.disableViaMobile !== true;
+			const viaMobile = (this as any).clientSettings.disableViaMobile !== true;
 			(this as any).api('notes/create', {
 				text: this.text == '' ? undefined : this.text,
 				mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
diff --git a/src/client/app/mobile/views/components/user-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue
index ee983a969c..ccdf8f64d2 100644
--- a/src/client/app/mobile/views/components/user-list-timeline.vue
+++ b/src/client/app/mobile/views/components/user-list-timeline.vue
@@ -46,8 +46,8 @@ export default Vue.extend({
 				(this as any).api('notes/user-list-timeline', {
 					listId: this.list.id,
 					limit: fetchLimit + 1,
-					includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-					includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+					includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+					includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 				}).then(notes => {
 					if (notes.length == fetchLimit + 1) {
 						notes.pop();
@@ -66,8 +66,8 @@ export default Vue.extend({
 				listId: this.list.id,
 				limit: fetchLimit + 1,
 				untilId: (this.$refs.timeline as any).tail().id,
-				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+				includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+				includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 			}).then(notes => {
 				if (notes.length == fetchLimit + 1) {
 					notes.pop();
diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue
index 23a83b5e3a..d258360911 100644
--- a/src/client/app/mobile/views/components/user-preview.vue
+++ b/src/client/app/mobile/views/components/user-preview.vue
@@ -1,8 +1,6 @@
 <template>
 <div class="mk-user-preview">
-	<router-link class="avatar-anchor" :to="user | userPage">
-		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-	</router-link>
+	<mk-avatar class="avatar" :user="user"/>
 	<div class="main">
 		<header>
 			<router-link class="name" :to="user | userPage">{{ user | userName }}</router-link>
@@ -40,26 +38,19 @@ export default Vue.extend({
 		display block
 		clear both
 
-	> .avatar-anchor
+	> .avatar
 		display block
 		float left
 		margin 0 10px 0 0
+		width 48px
+		height 48px
+		border-radius 6px
 
 		@media (min-width 500px)
 			margin-right 16px
-
-		> .avatar
-			display block
-			width 48px
-			height 48px
-			margin 0
-			border-radius 6px
-			vertical-align bottom
-
-			@media (min-width 500px)
-				width 58px
-				height 58px
-				border-radius 8px
+			width 58px
+			height 58px
+			border-radius 8px
 
 	> .main
 		float left
diff --git a/src/client/app/mobile/views/pages/dashboard.vue b/src/client/app/mobile/views/pages/dashboard.vue
index 53fe33ee8f..a5ca6cb4a2 100644
--- a/src/client/app/mobile/views/pages/dashboard.vue
+++ b/src/client/app/mobile/views/pages/dashboard.vue
@@ -64,8 +64,8 @@ export default Vue.extend({
 		};
 	},
 	created() {
-		if ((this as any).os.i.clientSettings.mobileHome == null) {
-			Vue.set((this as any).os.i.clientSettings, 'mobileHome', [{
+		if ((this as any).clientSettings.mobileHome == null) {
+			Vue.set((this as any).clientSettings, 'mobileHome', [{
 				name: 'calendar',
 				id: 'a', data: {}
 			}, {
@@ -87,14 +87,14 @@ export default Vue.extend({
 				name: 'version',
 				id: 'g', data: {}
 			}]);
-			this.widgets = (this as any).os.i.clientSettings.mobileHome;
+			this.widgets = (this as any).clientSettings.mobileHome;
 			this.saveHome();
 		} else {
-			this.widgets = (this as any).os.i.clientSettings.mobileHome;
+			this.widgets = (this as any).clientSettings.mobileHome;
 		}
 
-		this.$watch('os.i.clientSettings', i => {
-			this.widgets = (this as any).os.i.clientSettings.mobileHome;
+		this.$watch('clientSettings', i => {
+			this.widgets = (this as any).clientSettings.mobileHome;
 		}, {
 			deep: true
 		});
@@ -107,15 +107,15 @@ export default Vue.extend({
 	methods: {
 		onHomeUpdated(data) {
 			if (data.home) {
-				(this as any).os.i.clientSettings.mobileHome = data.home;
+				(this as any).clientSettings.mobileHome = data.home;
 				this.widgets = data.home;
 			} else {
-				const w = (this as any).os.i.clientSettings.mobileHome.find(w => w.id == data.id);
+				const w = (this as any).clientSettings.mobileHome.find(w => w.id == data.id);
 				if (w != null) {
 					w.data = data.data;
 					this.$refs[w.id][0].preventSave = true;
 					this.$refs[w.id][0].props = w.data;
-					this.widgets = (this as any).os.i.clientSettings.mobileHome;
+					this.widgets = (this as any).clientSettings.mobileHome;
 				}
 			}
 		},
@@ -144,7 +144,7 @@ export default Vue.extend({
 			this.saveHome();
 		},
 		saveHome() {
-			(this as any).os.i.clientSettings.mobileHome = this.widgets;
+			(this as any).clientSettings.mobileHome = this.widgets;
 			(this as any).api('i/update_mobile_home', {
 				home: this.widgets
 			});
diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue
index 5f4bd6dcd8..4c1c344db1 100644
--- a/src/client/app/mobile/views/pages/home.timeline.vue
+++ b/src/client/app/mobile/views/pages/home.timeline.vue
@@ -92,8 +92,8 @@ export default Vue.extend({
 				(this as any).api(this.endpoint, {
 					limit: fetchLimit + 1,
 					untilDate: this.date ? this.date.getTime() : undefined,
-					includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-					includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+					includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+					includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 				}).then(notes => {
 					if (notes.length == fetchLimit + 1) {
 						notes.pop();
@@ -114,8 +114,8 @@ export default Vue.extend({
 			(this as any).api(this.endpoint, {
 				limit: fetchLimit + 1,
 				untilId: (this.$refs.timeline as any).tail().id,
-				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
-				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+				includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+				includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
 			}).then(notes => {
 				if (notes.length == fetchLimit + 1) {
 					notes.pop();
diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
index 485996870e..4d236d7aa5 100644
--- a/src/client/app/mobile/views/pages/welcome.vue
+++ b/src/client/app/mobile/views/pages/welcome.vue
@@ -22,9 +22,7 @@
 			<mk-welcome-timeline/>
 		</div>
 		<div class="users">
-			<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${user.username}`">
-				<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
-			</router-link>
+			<mk-avatar class="avatar" :key="user.id" :user="user"/>
 		</div>
 		<footer>
 			<small>{{ copyright }}</small>
diff --git a/src/client/app/mobile/views/widgets/activity.vue b/src/client/app/mobile/views/widgets/activity.vue
index 48dcafb3ed..7763be41f5 100644
--- a/src/client/app/mobile/views/widgets/activity.vue
+++ b/src/client/app/mobile/views/widgets/activity.vue
@@ -21,6 +21,7 @@ export default define({
 	methods: {
 		func() {
 			this.props.compact = !this.props.compact;
+			this.save();
 		}
 	}
 });
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
new file mode 100644
index 0000000000..706fd65558
--- /dev/null
+++ b/src/client/app/store.ts
@@ -0,0 +1,90 @@
+import Vuex from 'vuex';
+import MiOS from './common/mios';
+
+const defaultSettings = {
+	home: [],
+	fetchOnScroll: true,
+	showMaps: true,
+	showPostFormOnTopOfTl: false,
+	circleIcons: true,
+	gradientWindowHeader: false,
+	showReplyTarget: true,
+	showMyRenotes: true,
+	showRenotedMyNotes: true
+};
+
+export default (os: MiOS) => new Vuex.Store({
+	plugins: [store => {
+		store.subscribe((mutation, state) => {
+			if (mutation.type.startsWith('settings/')) {
+				localStorage.setItem('settings', JSON.stringify(state.settings.data));
+			}
+		});
+	}],
+
+	state: {
+		uiHeaderHeight: 0
+	},
+
+	mutations: {
+		setUiHeaderHeight(state, height) {
+			state.uiHeaderHeight = height;
+		}
+	},
+
+	modules: {
+		settings: {
+			namespaced: true,
+
+			state: {
+				data: defaultSettings
+			},
+
+			mutations: {
+				init(state, settings) {
+					state.data = settings;
+				},
+
+				set(state, x: { key: string; value: any }) {
+					state.data[x.key] = x.value;
+				},
+
+				setHome(state, data) {
+					state.data.home = data;
+				},
+
+				setHomeWidget(state, x) {
+					const w = state.data.home.find(w => w.id == x.id);
+					if (w) {
+						w.data = x.data;
+					}
+				},
+
+				addHomeWidget(state, widget) {
+					state.data.home.unshift(widget);
+				}
+			},
+
+			actions: {
+				set(ctx, x) {
+					ctx.commit('set', x);
+
+					if (os.isSignedIn) {
+						os.api('i/update_client_setting', {
+							name: x.key,
+							value: x.value
+						});
+					}
+				},
+
+				addHomeWidget(ctx, widget) {
+					ctx.commit('addHomeWidget', widget);
+
+					os.api('i/update_home', {
+						home: ctx.state.data.home
+					});
+				}
+			}
+		}
+	}
+});
diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts
index 2edc2104d2..278eb1cbeb 100644
--- a/src/server/api/endpoints/i/update_client_setting.ts
+++ b/src/server/api/endpoints/i/update_client_setting.ts
@@ -24,16 +24,11 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 		$set: x
 	});
 
-	// Serialize
-	user.clientSettings[name] = value;
-	const iObj = await pack(user, user, {
-		detail: true,
-		includeSecrets: true
-	});
-
-	// Send response
-	res(iObj);
+	res();
 
-	// Publish i updated event
-	event(user._id, 'i_updated', iObj);
+	// Publish event
+	event(user._id, 'clientSettingUpdated', {
+		key: name,
+		value
+	});
 });
diff --git a/src/server/index.ts b/src/server/index.ts
index 594f40c22f..4c5aab9a22 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -26,9 +26,9 @@ if (process.env.NODE_ENV != 'production') {
 	app.use(logger());
 
 	// Delay
-	app.use(slow({
-		delay: 1000
-	}));
+	//app.use(slow({
+	//	delay: 1000
+	//}));
 }
 
 // Compress response
-- 
cgit v1.2.3-freya

