From d14a7922b99c4e7f0399acd8000126020d4a3b0f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Feb 2018 04:29:09 +0900
Subject: [PATCH 001/286] wip

---
 src/web/app/common/tags/url.tag | 54 ----------------------------
 src/web/app/common/tags/url.vue | 63 +++++++++++++++++++++++++++++++++
 2 files changed, 63 insertions(+), 54 deletions(-)
 delete mode 100644 src/web/app/common/tags/url.tag
 create mode 100644 src/web/app/common/tags/url.vue

diff --git a/src/web/app/common/tags/url.tag b/src/web/app/common/tags/url.tag
deleted file mode 100644
index 2690afc5d..000000000
--- a/src/web/app/common/tags/url.tag
+++ /dev/null
@@ -1,54 +0,0 @@
-<mk-url>
-	<a href={ url } target={ opts.target }>
-		<span class="schema">{ schema }//</span>
-		<span class="hostname">{ hostname }</span>
-		<span class="port" if={ port != '' }>:{ port }</span>
-		<span class="pathname" if={ pathname != '' }>{ pathname }</span>
-		<span class="query">{ query }</span>
-		<span class="hash">{ hash }</span>
-		%fa:external-link-square-alt%
-	</a>
-	<style>
-		:scope
-			word-break break-all
-
-			> a
-				> [data-fa]
-					padding-left 2px
-					font-size .9em
-					font-weight 400
-					font-style normal
-
-				> .schema
-					opacity 0.5
-
-				> .hostname
-					font-weight bold
-
-				> .pathname
-					opacity 0.8
-
-				> .query
-					opacity 0.5
-
-				> .hash
-					font-style italic
-
-	</style>
-	<script>
-		this.url = this.opts.href;
-
-		this.on('before-mount', () => {
-			const url = new URL(this.url);
-
-			this.schema = url.protocol;
-			this.hostname = url.hostname;
-			this.port = url.port;
-			this.pathname = url.pathname;
-			this.query = url.search;
-			this.hash = url.hash;
-
-			this.update();
-		});
-	</script>
-</mk-url>
diff --git a/src/web/app/common/tags/url.vue b/src/web/app/common/tags/url.vue
new file mode 100644
index 000000000..fdc8a1cb2
--- /dev/null
+++ b/src/web/app/common/tags/url.vue
@@ -0,0 +1,63 @@
+<template>
+	<a :href="url" :target="target">
+		<span class="schema">{{ schema }}//</span>
+		<span class="hostname">{{ hostname }}</span>
+		<span class="port" v-if="port != ''">:{{ port }}</span>
+		<span class="pathname" v-if="pathname != ''">{{ pathname }}</span>
+		<span class="query">{{ query }}</span>
+		<span class="hash">{{ hash }}</span>
+		%fa:external-link-square-alt%
+	</a>
+</template>
+
+<script lang="typescript">
+	export default {
+		props: ['url', 'target'],
+		created: function() {
+			const url = new URL(this.url);
+
+			this.schema = url.protocol;
+			this.hostname = url.hostname;
+			this.port = url.port;
+			this.pathname = url.pathname;
+			this.query = url.search;
+			this.hash = url.hash;
+		},
+		data: {
+			schema: null,
+			hostname: null,
+			port: null,
+			pathname: null,
+			query: null,
+			hash: null
+		}
+	};
+</script>
+
+<style lang="stylus" scoped>
+	:scope
+		word-break break-all
+
+	> a
+		> [data-fa]
+			padding-left 2px
+			font-size .9em
+			font-weight 400
+			font-style normal
+
+		> .schema
+			opacity 0.5
+
+		> .hostname
+			font-weight bold
+
+		> .pathname
+			opacity 0.8
+
+		> .query
+			opacity 0.5
+
+		> .hash
+			font-style italic
+
+</style>

From 18e1628e2ab0a5537773c4192ebd5625fce961ff Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 5 Feb 2018 14:25:19 +0900
Subject: [PATCH 002/286] wip

---
 .../app/common/tags/{time.tag => time.vue}    |  59 +++++----
 src/web/app/common/tags/url-preview.tag       | 117 -----------------
 src/web/app/common/tags/url-preview.vue       | 124 ++++++++++++++++++
 3 files changed, 154 insertions(+), 146 deletions(-)
 rename src/web/app/common/tags/{time.tag => time.vue} (56%)
 delete mode 100644 src/web/app/common/tags/url-preview.tag
 create mode 100644 src/web/app/common/tags/url-preview.vue

diff --git a/src/web/app/common/tags/time.tag b/src/web/app/common/tags/time.vue
similarity index 56%
rename from src/web/app/common/tags/time.tag
rename to src/web/app/common/tags/time.vue
index b0d7d2453..14f38eb2d 100644
--- a/src/web/app/common/tags/time.tag
+++ b/src/web/app/common/tags/time.vue
@@ -1,36 +1,38 @@
-<mk-time>
-	<time datetime={ opts.time }>
-		<span if={ mode == 'relative' }>{ relative }</span>
-		<span if={ mode == 'absolute' }>{ absolute }</span>
-		<span if={ mode == 'detail' }>{ absolute } ({ relative })</span>
+<template>
+	<time>
+		<span v-if=" mode == 'relative' ">{{ relative }}</span>
+		<span v-if=" mode == 'absolute' ">{{ absolute }}</span>
+		<span v-if=" mode == 'detail' ">{{ absolute }} ({{ relative }})</span>
 	</time>
-	<script>
-		this.time = new Date(this.opts.time);
-		this.mode = this.opts.mode || 'relative';
-		this.tickid = null;
+</template>
 
-		this.absolute =
-			this.time.getFullYear()    + '年' +
-			(this.time.getMonth() + 1) + '月' +
-			this.time.getDate()        + '日' +
-			' ' +
-			this.time.getHours()       + '時' +
-			this.time.getMinutes()     + '分';
+<script>
+	export default {
+		props: ['time', 'mode'],
+		data: {
+			mode: 'relative',
+			tickId: null,
+		},
+		created: function() {
+			this.absolute =
+				this.time.getFullYear()    + '年' +
+				(this.time.getMonth() + 1) + '月' +
+				this.time.getDate()        + '日' +
+				' ' +
+				this.time.getHours()       + '時' +
+				this.time.getMinutes()     + '分';
 
-		this.on('mount', () => {
 			if (this.mode == 'relative' || this.mode == 'detail') {
 				this.tick();
-				this.tickid = setInterval(this.tick, 1000);
+				this.tickId = setInterval(this.tick, 1000);
 			}
-		});
-
-		this.on('unmount', () => {
+		},
+		destroyed: function() {
 			if (this.mode === 'relative' || this.mode === 'detail') {
-				clearInterval(this.tickid);
+				clearInterval(this.tickId);
 			}
-		});
-
-		this.tick = () => {
+		},
+		tick: function() {
 			const now = new Date();
 			const ago = (now - this.time) / 1000/*ms*/;
 			this.relative =
@@ -44,7 +46,6 @@
 				ago >= 0        ? '%i18n:common.time.just_now%' :
 				ago <  0        ? '%i18n:common.time.future%' :
 				'%i18n:common.time.unknown%';
-			this.update();
-		};
-	</script>
-</mk-time>
+		}
+	};
+</script>
diff --git a/src/web/app/common/tags/url-preview.tag b/src/web/app/common/tags/url-preview.tag
deleted file mode 100644
index 7dbdd8fea..000000000
--- a/src/web/app/common/tags/url-preview.tag
+++ /dev/null
@@ -1,117 +0,0 @@
-<mk-url-preview>
-	<a href={ url } target="_blank" title={ url } if={ !loading }>
-		<div class="thumbnail" if={ thumbnail } style={ 'background-image: url(' + thumbnail + ')' }></div>
-		<article>
-			<header>
-				<h1>{ title }</h1>
-			</header>
-			<p>{ description }</p>
-			<footer>
-				<img class="icon" if={ icon } src={ icon }/>
-				<p>{ sitename }</p>
-			</footer>
-		</article>
-	</a>
-	<style>
-		:scope
-			display block
-			font-size 16px
-
-			> a
-				display block
-				border solid 1px #eee
-				border-radius 4px
-				overflow hidden
-
-				&:hover
-					text-decoration none
-					border-color #ddd
-
-					> article > header > h1
-						text-decoration underline
-
-				> .thumbnail
-					position absolute
-					width 100px
-					height 100%
-					background-position center
-					background-size cover
-
-					& + article
-						left 100px
-						width calc(100% - 100px)
-
-				> article
-					padding 16px
-
-					> header
-						margin-bottom 8px
-
-						> h1
-							margin 0
-							font-size 1em
-							color #555
-
-					> p
-						margin 0
-						color #777
-						font-size 0.8em
-
-					> footer
-						margin-top 8px
-						height 16px
-
-						> img
-							display inline-block
-							width 16px
-							height 16px
-							margin-right 4px
-							vertical-align top
-
-						> p
-							display inline-block
-							margin 0
-							color #666
-							font-size 0.8em
-							line-height 16px
-							vertical-align top
-
-			@media (max-width 500px)
-				font-size 8px
-
-				> a
-					border none
-
-					> .thumbnail
-						width 70px
-
-						& + article
-							left 70px
-							width calc(100% - 70px)
-
-					> article
-						padding 8px
-
-	</style>
-	<script>
-		this.mixin('api');
-
-		this.url = this.opts.url;
-		this.loading = true;
-
-		this.on('mount', () => {
-			fetch('/api:url?url=' + this.url).then(res => {
-				res.json().then(info => {
-					this.title = info.title;
-					this.description = info.description;
-					this.thumbnail = info.thumbnail;
-					this.icon = info.icon;
-					this.sitename = info.sitename;
-
-					this.loading = false;
-					this.update();
-				});
-			});
-		});
-	</script>
-</mk-url-preview>
diff --git a/src/web/app/common/tags/url-preview.vue b/src/web/app/common/tags/url-preview.vue
new file mode 100644
index 000000000..45a718d3e
--- /dev/null
+++ b/src/web/app/common/tags/url-preview.vue
@@ -0,0 +1,124 @@
+<template>
+	<a :href="url" target="_blank" :title="url" v-if="!fetching">
+		<div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div>
+		<article>
+			<header>
+				<h1>{{ title }}</h1>
+			</header>
+			<p>{{ description }}</p>
+			<footer>
+				<img class="icon" v-if="icon" :src="icon"/>
+				<p>{{ sitename }}</p>
+			</footer>
+		</article>
+	</a>
+</template>
+
+<script lang="typescript">
+	export default {
+		props: ['url'],
+		created: function() {
+			fetch('/api:url?url=' + this.url).then(res => {
+				res.json().then(info => {
+					this.title = info.title;
+					this.description = info.description;
+					this.thumbnail = info.thumbnail;
+					this.icon = info.icon;
+					this.sitename = info.sitename;
+
+					this.fetching = false;
+				});
+			});
+		},
+		data: {
+			fetching: true,
+			title: null,
+			description: null,
+			thumbnail: null,
+			icon: null,
+			sitename: null
+		}
+	};
+</script>
+
+<style lang="stylus" scoped>
+	:scope
+		display block
+		font-size 16px
+
+		> a
+			display block
+			border solid 1px #eee
+			border-radius 4px
+			overflow hidden
+
+			&:hover
+				text-decoration none
+				border-color #ddd
+
+				> article > header > h1
+					text-decoration underline
+
+			> .thumbnail
+				position absolute
+				width 100px
+				height 100%
+				background-position center
+				background-size cover
+
+				& + article
+					left 100px
+					width calc(100% - 100px)
+
+			> article
+				padding 16px
+
+				> header
+					margin-bottom 8px
+
+					> h1
+						margin 0
+						font-size 1em
+						color #555
+
+				> p
+					margin 0
+					color #777
+					font-size 0.8em
+
+				> footer
+					margin-top 8px
+					height 16px
+
+					> img
+						display inline-block
+						width 16px
+						height 16px
+						margin-right 4px
+						vertical-align top
+
+					> p
+						display inline-block
+						margin 0
+						color #666
+						font-size 0.8em
+						line-height 16px
+						vertical-align top
+
+		@media (max-width 500px)
+			font-size 8px
+
+			> a
+				border none
+
+				> .thumbnail
+					width 70px
+
+					& + article
+						left 70px
+						width calc(100% - 70px)
+
+				> article
+					padding 8px
+
+</style>

From 0c2b79acedc08fa0702b52d612aa0b92f67f1573 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Feb 2018 15:16:01 +0900
Subject: [PATCH 003/286] wip

---
 src/web/app/auth/tags/form.tag                |   4 +-
 src/web/app/ch/tags/channel.tag               |  14 +-
 src/web/app/ch/tags/index.tag                 |   2 +-
 src/web/app/common/tags/error.tag             |   4 +-
 src/web/app/common/tags/messaging/form.tag    |   2 +-
 src/web/app/common/tags/messaging/index.tag   |   4 +-
 src/web/app/common/tags/messaging/room.tag    |   2 +-
 src/web/app/common/tags/poll-editor.tag       |   6 +-
 src/web/app/common/tags/poll.tag              |   4 +-
 src/web/app/common/tags/post-menu.tag         |   6 +-
 src/web/app/common/tags/reaction-picker.tag   | 184 ----------------
 src/web/app/common/tags/reaction-picker.vue   | 202 ++++++++++++++++++
 src/web/app/common/tags/signin-history.tag    |   2 +-
 src/web/app/common/tags/signup.tag            |   2 +-
 src/web/app/common/tags/stream-indicator.tag  |  78 -------
 src/web/app/common/tags/stream-indicator.vue  |  74 +++++++
 src/web/app/common/tags/twitter-setting.tag   |   4 +-
 .../desktop/tags/autocomplete-suggestion.tag  |   2 +-
 .../app/desktop/tags/big-follow-button.tag    |   2 +-
 src/web/app/desktop/tags/crop-window.tag      |   6 +-
 .../app/desktop/tags/detailed-post-window.tag |   2 +-
 src/web/app/desktop/tags/dialog.tag           |   4 +-
 src/web/app/desktop/tags/donation.tag         |   2 +-
 .../desktop/tags/drive/base-contextmenu.tag   |   6 +-
 src/web/app/desktop/tags/drive/browser.tag    |   2 +-
 .../desktop/tags/drive/file-contextmenu.tag   |  14 +-
 src/web/app/desktop/tags/drive/file.tag       |   2 +-
 .../desktop/tags/drive/folder-contextmenu.tag |   8 +-
 src/web/app/desktop/tags/drive/folder.tag     |   2 +-
 src/web/app/desktop/tags/drive/nav-folder.tag |   2 +-
 src/web/app/desktop/tags/follow-button.tag    |   2 +-
 .../app/desktop/tags/following-setuper.tag    |   4 +-
 .../desktop/tags/home-widgets/broadcast.tag   |   2 +-
 .../app/desktop/tags/home-widgets/channel.tag |   4 +-
 .../desktop/tags/home-widgets/mentions.tag    |   2 +-
 .../tags/home-widgets/notifications.tag       |   2 +-
 .../desktop/tags/home-widgets/post-form.tag   |   2 +-
 .../app/desktop/tags/home-widgets/profile.tag |   4 +-
 .../tags/home-widgets/recommended-polls.tag   |   2 +-
 .../desktop/tags/home-widgets/rss-reader.tag  |   2 +-
 .../app/desktop/tags/home-widgets/server.tag  |   2 +-
 .../desktop/tags/home-widgets/slideshow.tag   |   4 +-
 .../app/desktop/tags/home-widgets/trends.tag  |   2 +-
 .../tags/home-widgets/user-recommendation.tag |   2 +-
 src/web/app/desktop/tags/home.tag             |   2 +-
 src/web/app/desktop/tags/images.tag           |   4 +-
 src/web/app/desktop/tags/input-dialog.tag     |   4 +-
 src/web/app/desktop/tags/notifications.tag    |   2 +-
 src/web/app/desktop/tags/pages/entrance.tag   |   6 +-
 .../app/desktop/tags/pages/selectdrive.tag    |   6 +-
 src/web/app/desktop/tags/post-detail.tag      |  10 +-
 src/web/app/desktop/tags/post-form.tag        |  12 +-
 src/web/app/desktop/tags/repost-form.tag      |   6 +-
 .../tags/select-file-from-drive-window.tag    |   6 +-
 .../tags/select-folder-from-drive-window.tag  |   4 +-
 .../desktop/tags/set-avatar-suggestion.tag    |   4 +-
 .../desktop/tags/set-banner-suggestion.tag    |   4 +-
 src/web/app/desktop/tags/settings.tag         |  14 +-
 src/web/app/desktop/tags/timeline.tag         |  10 +-
 src/web/app/desktop/tags/ui.tag               |  14 +-
 src/web/app/desktop/tags/user-timeline.tag    |   2 +-
 src/web/app/desktop/tags/user.tag             |  10 +-
 src/web/app/desktop/tags/users-list.tag       |   6 +-
 src/web/app/desktop/tags/widgets/activity.tag |   2 +-
 src/web/app/desktop/tags/widgets/calendar.tag |   6 +-
 src/web/app/desktop/tags/window.tag           |   6 +-
 src/web/app/dev/tags/new-app-form.tag         |   2 +-
 .../app/mobile/tags/drive-folder-selector.tag |   4 +-
 src/web/app/mobile/tags/drive-selector.tag    |   4 +-
 src/web/app/mobile/tags/drive.tag             |   6 +-
 src/web/app/mobile/tags/drive/file-viewer.tag |   6 +-
 src/web/app/mobile/tags/drive/file.tag        |   2 +-
 src/web/app/mobile/tags/drive/folder.tag      |   2 +-
 src/web/app/mobile/tags/follow-button.tag     |   2 +-
 src/web/app/mobile/tags/init-following.tag    |   4 +-
 src/web/app/mobile/tags/notifications.tag     |   2 +-
 src/web/app/mobile/tags/page/entrance.tag     |   2 +-
 .../app/mobile/tags/page/entrance/signin.tag  |   2 +-
 .../app/mobile/tags/page/entrance/signup.tag  |   2 +-
 src/web/app/mobile/tags/page/selectdrive.tag  |   4 +-
 src/web/app/mobile/tags/page/settings.tag     |   2 +-
 .../app/mobile/tags/page/settings/profile.tag |  10 +-
 src/web/app/mobile/tags/post-detail.tag       |  10 +-
 src/web/app/mobile/tags/post-form.tag         |  14 +-
 src/web/app/mobile/tags/timeline.tag          |  10 +-
 src/web/app/mobile/tags/ui.tag                |   8 +-
 src/web/app/mobile/tags/user.tag              |   6 +-
 src/web/app/mobile/tags/users-list.tag        |   6 +-
 88 files changed, 474 insertions(+), 460 deletions(-)
 delete mode 100644 src/web/app/common/tags/reaction-picker.tag
 create mode 100644 src/web/app/common/tags/reaction-picker.vue
 delete mode 100644 src/web/app/common/tags/stream-indicator.tag
 create mode 100644 src/web/app/common/tags/stream-indicator.vue

diff --git a/src/web/app/auth/tags/form.tag b/src/web/app/auth/tags/form.tag
index 4a236f759..5bb27c269 100644
--- a/src/web/app/auth/tags/form.tag
+++ b/src/web/app/auth/tags/form.tag
@@ -26,8 +26,8 @@
 		</section>
 	</div>
 	<div class="action">
-		<button onclick={ cancel }>キャンセル</button>
-		<button onclick={ accept }>アクセスを許可</button>
+		<button @click="cancel">キャンセル</button>
+		<button @click="accept">アクセスを許可</button>
 	</div>
 	<style>
 		:scope
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index cc8ce1ed9..7e76778f9 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -5,8 +5,8 @@
 		<h1>{ channel.title }</h1>
 
 		<div if={ SIGNIN }>
-			<p if={ channel.is_watching }>このチャンネルをウォッチしています <a onclick={ unwatch }>ウォッチ解除</a></p>
-			<p if={ !channel.is_watching }><a onclick={ watch }>このチャンネルをウォッチする</a></p>
+			<p if={ channel.is_watching }>このチャンネルをウォッチしています <a @click="unwatch">ウォッチ解除</a></p>
+			<p if={ !channel.is_watching }><a @click="watch">このチャンネルをウォッチする</a></p>
 		</div>
 
 		<div class="share">
@@ -164,7 +164,7 @@
 
 <mk-channel-post>
 	<header>
-		<a class="index" onclick={ reply }>{ post.index }:</a>
+		<a class="index" @click="reply">{ post.index }:</a>
 		<a class="name" href={ _URL_ + '/' + post.user.username }><b>{ post.user.name }</b></a>
 		<mk-time time={ post.created_at }/>
 		<mk-time time={ post.created_at } mode="detail"/>
@@ -241,12 +241,12 @@
 </mk-channel-post>
 
 <mk-channel-form>
-	<p if={ reply }><b>&gt;&gt;{ reply.index }</b> ({ reply.user.name }): <a onclick={ clearReply }>[x]</a></p>
+	<p if={ reply }><b>&gt;&gt;{ reply.index }</b> ({ reply.user.name }): <a @click="clearReply">[x]</a></p>
 	<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder="%i18n:ch.tags.mk-channel-form.textarea%"></textarea>
 	<div class="actions">
-		<button onclick={ selectFile }>%fa:upload%%i18n:ch.tags.mk-channel-form.upload%</button>
-		<button onclick={ drive }>%fa:cloud%%i18n:ch.tags.mk-channel-form.drive%</button>
-		<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } onclick={ post }>
+		<button @click="selectFile">%fa:upload%%i18n:ch.tags.mk-channel-form.upload%</button>
+		<button @click="drive">%fa:cloud%%i18n:ch.tags.mk-channel-form.drive%</button>
+		<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } @click="post">
 			<virtual if={ !wait }>%fa:paper-plane%</virtual>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.post%' }<mk-ellipsis if={ wait }/>
 		</button>
 	</div>
diff --git a/src/web/app/ch/tags/index.tag b/src/web/app/ch/tags/index.tag
index 5f3871802..489f21148 100644
--- a/src/web/app/ch/tags/index.tag
+++ b/src/web/app/ch/tags/index.tag
@@ -1,7 +1,7 @@
 <mk-index>
 	<mk-header/>
 	<hr>
-	<button onclick={ n }>%i18n:ch.tags.mk-index.new%</button>
+	<button @click="n">%i18n:ch.tags.mk-index.new%</button>
 	<hr>
 	<ul if={ channels }>
 		<li each={ channels }><a href={ '/' + this.id }>{ this.title }</a></li>
diff --git a/src/web/app/common/tags/error.tag b/src/web/app/common/tags/error.tag
index a5b8d1489..07ba61161 100644
--- a/src/web/app/common/tags/error.tag
+++ b/src/web/app/common/tags/error.tag
@@ -3,12 +3,12 @@
 	<h1>%i18n:common.tags.mk-error.title%</h1>
 	<p class="text">{
 		'%i18n:common.tags.mk-error.description%'.substr(0, '%i18n:common.tags.mk-error.description%'.indexOf('{'))
-	}<a onclick={ reload }>{
+	}<a @click="reload">{
 		'%i18n:common.tags.mk-error.description%'.match(/\{(.+?)\}/)[1]
 	}</a>{
 		'%i18n:common.tags.mk-error.description%'.substr('%i18n:common.tags.mk-error.description%'.indexOf('}') + 1)
 	}</p>
-	<button if={ !troubleshooting } onclick={ troubleshoot }>%i18n:common.tags.mk-error.troubleshoot%</button>
+	<button if={ !troubleshooting } @click="troubleshoot">%i18n:common.tags.mk-error.troubleshoot%</button>
 	<mk-troubleshooter if={ troubleshooting }/>
 	<p class="thanks">%i18n:common.tags.mk-error.thanks%</p>
 	<style>
diff --git a/src/web/app/common/tags/messaging/form.tag b/src/web/app/common/tags/messaging/form.tag
index 7b133a71c..a5de32e3f 100644
--- a/src/web/app/common/tags/messaging/form.tag
+++ b/src/web/app/common/tags/messaging/form.tag
@@ -2,7 +2,7 @@
 	<textarea ref="text" onkeypress={ onkeypress } onpaste={ onpaste } placeholder="%i18n:common.input-message-here%"></textarea>
 	<div class="files"></div>
 	<mk-uploader ref="uploader"/>
-	<button class="send" onclick={ send } disabled={ sending } title="%i18n:common.send%">
+	<button class="send" @click="send" disabled={ sending } title="%i18n:common.send%">
 		<virtual if={ !sending }>%fa:paper-plane%</virtual><virtual if={ sending }>%fa:spinner .spin%</virtual>
 	</button>
 	<button class="attach-from-local" type="button" title="%i18n:common.tags.mk-messaging-form.attach-from-local%">
diff --git a/src/web/app/common/tags/messaging/index.tag b/src/web/app/common/tags/messaging/index.tag
index d26cec6cd..547727da2 100644
--- a/src/web/app/common/tags/messaging/index.tag
+++ b/src/web/app/common/tags/messaging/index.tag
@@ -6,7 +6,7 @@
 		</div>
 		<div class="result">
 			<ol class="users" if={ searchResult.length > 0 } ref="searchResult">
-				<li each={ user, i in searchResult } onkeydown={ parent.onSearchResultKeydown.bind(null, i) } onclick={ user._click } tabindex="-1">
+				<li each={ user, i in searchResult } onkeydown={ parent.onSearchResultKeydown.bind(null, i) } @click="user._click" tabindex="-1">
 					<img class="avatar" src={ user.avatar_url + '?thumbnail&size=32' } alt=""/>
 					<span class="name">{ user.name }</span>
 					<span class="username">@{ user.username }</span>
@@ -16,7 +16,7 @@
 	</div>
 	<div class="history" if={ history.length > 0 }>
 		<virtual each={ history }>
-			<a class="user" data-is-me={ is_me } data-is-read={ is_read } onclick={ _click }>
+			<a class="user" data-is-me={ is_me } data-is-read={ is_read } @click="_click">
 				<div>
 					<img class="avatar" src={ (is_me ? recipient.avatar_url : user.avatar_url) + '?thumbnail&size=64' } alt=""/>
 					<header>
diff --git a/src/web/app/common/tags/messaging/room.tag b/src/web/app/common/tags/messaging/room.tag
index 7b4d1be56..a42e0ea94 100644
--- a/src/web/app/common/tags/messaging/room.tag
+++ b/src/web/app/common/tags/messaging/room.tag
@@ -3,7 +3,7 @@
 		<p class="init" if={ init }>%fa:spinner .spin%%i18n:common.loading%</p>
 		<p class="empty" if={ !init && messages.length == 0 }>%fa:info-circle%%i18n:common.tags.mk-messaging-room.empty%</p>
 		<p class="no-history" if={ !init && messages.length > 0 && !moreMessagesIsInStock }>%fa:flag%%i18n:common.tags.mk-messaging-room.no-history%</p>
-		<button class="more { fetching: fetchingMoreMessages }" if={ moreMessagesIsInStock } onclick={ fetchMoreMessages } disabled={ fetchingMoreMessages }>
+		<button class="more { fetching: fetchingMoreMessages }" if={ moreMessagesIsInStock } @click="fetchMoreMessages" disabled={ fetchingMoreMessages }>
 			<virtual if={ fetchingMoreMessages }>%fa:spinner .pulse .fw%</virtual>{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' }
 		</button>
 		<virtual each={ message, i in messages }>
diff --git a/src/web/app/common/tags/poll-editor.tag b/src/web/app/common/tags/poll-editor.tag
index e79209e9b..73e783ddb 100644
--- a/src/web/app/common/tags/poll-editor.tag
+++ b/src/web/app/common/tags/poll-editor.tag
@@ -5,13 +5,13 @@
 	<ul ref="choices">
 		<li each={ choice, i in choices }>
 			<input value={ choice } oninput={ oninput.bind(null, i) } placeholder={ '%i18n:common.tags.mk-poll-editor.choice-n%'.replace('{}', i + 1) }>
-			<button onclick={ remove.bind(null, i) } title="%i18n:common.tags.mk-poll-editor.remove%">
+			<button @click="remove.bind(null, i)" title="%i18n:common.tags.mk-poll-editor.remove%">
 				%fa:times%
 			</button>
 		</li>
 	</ul>
-	<button class="add" if={ choices.length < 10 } onclick={ add }>%i18n:common.tags.mk-poll-editor.add%</button>
-	<button class="destroy" onclick={ destroy } title="%i18n:common.tags.mk-poll-editor.destroy%">
+	<button class="add" if={ choices.length < 10 } @click="add">%i18n:common.tags.mk-poll-editor.add%</button>
+	<button class="destroy" @click="destroy" title="%i18n:common.tags.mk-poll-editor.destroy%">
 		%fa:times%
 	</button>
 	<style>
diff --git a/src/web/app/common/tags/poll.tag b/src/web/app/common/tags/poll.tag
index 32542418a..3d0a559d0 100644
--- a/src/web/app/common/tags/poll.tag
+++ b/src/web/app/common/tags/poll.tag
@@ -1,6 +1,6 @@
 <mk-poll data-is-voted={ isVoted }>
 	<ul>
-		<li each={ poll.choices } onclick={ vote.bind(null, id) } class={ voted: voted } title={ !parent.isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', text) : '' }>
+		<li each={ poll.choices } @click="vote.bind(null, id)" class={ voted: voted } title={ !parent.isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', text) : '' }>
 			<div class="backdrop" style={ 'width:' + (parent.result ? (votes / parent.total * 100) : 0) + '%' }></div>
 			<span>
 				<virtual if={ is_voted }>%fa:check%</virtual>
@@ -12,7 +12,7 @@
 	<p if={ total > 0 }>
 		<span>{ '%i18n:common.tags.mk-poll.total-users%'.replace('{}', total) }</span>
 		・
-		<a if={ !isVoted } onclick={ toggleResult }>{ result ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }</a>
+		<a if={ !isVoted } @click="toggleResult">{ result ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }</a>
 		<span if={ isVoted }>%i18n:common.tags.mk-poll.voted%</span>
 	</p>
 	<style>
diff --git a/src/web/app/common/tags/post-menu.tag b/src/web/app/common/tags/post-menu.tag
index be4468a21..dd2a273d4 100644
--- a/src/web/app/common/tags/post-menu.tag
+++ b/src/web/app/common/tags/post-menu.tag
@@ -1,7 +1,7 @@
 <mk-post-menu>
-	<div class="backdrop" ref="backdrop" onclick={ close }></div>
+	<div class="backdrop" ref="backdrop" @click="close"></div>
 	<div class="popover { compact: opts.compact }" ref="popover">
-		<button if={ post.user_id === I.id } onclick={ pin }>%i18n:common.tags.mk-post-menu.pin%</button>
+		<button if={ post.user_id === I.id } @click="pin">%i18n:common.tags.mk-post-menu.pin%</button>
 		<div if={ I.is_pro && !post.is_category_verified }>
 			<select ref="categorySelect">
 				<option value="">%i18n:common.tags.mk-post-menu.select%</option>
@@ -12,7 +12,7 @@
 				<option value="gadgets">%i18n:common.post_categories.gadgets%</option>
 				<option value="photography">%i18n:common.post_categories.photography%</option>
 			</select>
-			<button onclick={ categorize }>%i18n:common.tags.mk-post-menu.categorize%</button>
+			<button @click="categorize">%i18n:common.tags.mk-post-menu.categorize%</button>
 		</div>
 	</div>
 	<style>
diff --git a/src/web/app/common/tags/reaction-picker.tag b/src/web/app/common/tags/reaction-picker.tag
deleted file mode 100644
index 458d16ec7..000000000
--- a/src/web/app/common/tags/reaction-picker.tag
+++ /dev/null
@@ -1,184 +0,0 @@
-<mk-reaction-picker>
-	<div class="backdrop" ref="backdrop" onclick={ close }></div>
-	<div class="popover { compact: opts.compact }" ref="popover">
-		<p if={ !opts.compact }>{ title }</p>
-		<div>
-			<button onclick={ react.bind(null, 'like') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>
-			<button onclick={ react.bind(null, 'love') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button>
-			<button onclick={ react.bind(null, 'laugh') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button>
-			<button onclick={ react.bind(null, 'hmm') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="4" title="%i18n:common.reactions.hmm%"><mk-reaction-icon reaction='hmm'/></button>
-			<button onclick={ react.bind(null, 'surprise') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="5" title="%i18n:common.reactions.surprise%"><mk-reaction-icon reaction='surprise'/></button>
-			<button onclick={ react.bind(null, 'congrats') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="6" title="%i18n:common.reactions.congrats%"><mk-reaction-icon reaction='congrats'/></button>
-			<button onclick={ react.bind(null, 'angry') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="4" title="%i18n:common.reactions.angry%"><mk-reaction-icon reaction='angry'/></button>
-			<button onclick={ react.bind(null, 'confused') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="5" title="%i18n:common.reactions.confused%"><mk-reaction-icon reaction='confused'/></button>
-			<button onclick={ react.bind(null, 'pudding') } onmouseover={ onmouseover } onmouseout={ onmouseout } tabindex="6" title="%i18n:common.reactions.pudding%"><mk-reaction-icon reaction='pudding'/></button>
-		</div>
-	</div>
-	<style>
-		$border-color = rgba(27, 31, 35, 0.15)
-
-		:scope
-			display block
-			position initial
-
-			> .backdrop
-				position fixed
-				top 0
-				left 0
-				z-index 10000
-				width 100%
-				height 100%
-				background rgba(0, 0, 0, 0.1)
-				opacity 0
-
-			> .popover
-				position absolute
-				z-index 10001
-				background #fff
-				border 1px solid $border-color
-				border-radius 4px
-				box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
-				transform scale(0.5)
-				opacity 0
-
-				$balloon-size = 16px
-
-				&:not(.compact)
-					margin-top $balloon-size
-					transform-origin center -($balloon-size)
-
-					&:before
-						content ""
-						display block
-						position absolute
-						top -($balloon-size * 2)
-						left s('calc(50% - %s)', $balloon-size)
-						border-top solid $balloon-size transparent
-						border-left solid $balloon-size transparent
-						border-right solid $balloon-size transparent
-						border-bottom solid $balloon-size $border-color
-
-					&:after
-						content ""
-						display block
-						position absolute
-						top -($balloon-size * 2) + 1.5px
-						left s('calc(50% - %s)', $balloon-size)
-						border-top solid $balloon-size transparent
-						border-left solid $balloon-size transparent
-						border-right solid $balloon-size transparent
-						border-bottom solid $balloon-size #fff
-
-				> p
-					display block
-					margin 0
-					padding 8px 10px
-					font-size 14px
-					color #586069
-					border-bottom solid 1px #e1e4e8
-
-				> div
-					padding 4px
-					width 240px
-					text-align center
-
-					> button
-						width 40px
-						height 40px
-						font-size 24px
-						border-radius 2px
-
-						&:hover
-							background #eee
-
-						&:active
-							background $theme-color
-							box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
-
-	</style>
-	<script>
-		import anime from 'animejs';
-
-		this.mixin('api');
-
-		this.post = this.opts.post;
-		this.source = this.opts.source;
-
-		const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%';
-
-		this.title = placeholder;
-
-		this.onmouseover = e => {
-			this.update({
-				title: e.target.title
-			});
-		};
-
-		this.onmouseout = () => {
-			this.update({
-				title: placeholder
-			});
-		};
-
-		this.on('mount', () => {
-			const rect = this.source.getBoundingClientRect();
-			const width = this.refs.popover.offsetWidth;
-			const height = this.refs.popover.offsetHeight;
-			if (this.opts.compact) {
-				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-				const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
-				this.refs.popover.style.left = (x - (width / 2)) + 'px';
-				this.refs.popover.style.top = (y - (height / 2)) + 'px';
-			} else {
-				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-				const y = rect.top + window.pageYOffset + this.source.offsetHeight;
-				this.refs.popover.style.left = (x - (width / 2)) + 'px';
-				this.refs.popover.style.top = y + 'px';
-			}
-
-			anime({
-				targets: this.refs.backdrop,
-				opacity: 1,
-				duration: 100,
-				easing: 'linear'
-			});
-
-			anime({
-				targets: this.refs.popover,
-				opacity: 1,
-				scale: [0.5, 1],
-				duration: 500
-			});
-		});
-
-		this.react = reaction => {
-			this.api('posts/reactions/create', {
-				post_id: this.post.id,
-				reaction: reaction
-			}).then(() => {
-				if (this.opts.cb) this.opts.cb();
-				this.unmount();
-			});
-		};
-
-		this.close = () => {
-			this.refs.backdrop.style.pointerEvents = 'none';
-			anime({
-				targets: this.refs.backdrop,
-				opacity: 0,
-				duration: 200,
-				easing: 'linear'
-			});
-
-			this.refs.popover.style.pointerEvents = 'none';
-			anime({
-				targets: this.refs.popover,
-				opacity: 0,
-				scale: 0.5,
-				duration: 200,
-				easing: 'easeInBack',
-				complete: () => this.unmount()
-			});
-		};
-	</script>
-</mk-reaction-picker>
diff --git a/src/web/app/common/tags/reaction-picker.vue b/src/web/app/common/tags/reaction-picker.vue
new file mode 100644
index 000000000..243039030
--- /dev/null
+++ b/src/web/app/common/tags/reaction-picker.vue
@@ -0,0 +1,202 @@
+<template>
+<div>
+	<div class="backdrop" ref="backdrop" @click="close"></div>
+	<div class="popover" :data-compact="compact" ref="popover">
+		<p if={ !opts.compact }>{ title }</p>
+		<div>
+			<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>
+			<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button>
+			<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button>
+			<button @click="react('hmm')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="4" title="%i18n:common.reactions.hmm%"><mk-reaction-icon reaction='hmm'/></button>
+			<button @click="react('surprise')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="5" title="%i18n:common.reactions.surprise%"><mk-reaction-icon reaction='surprise'/></button>
+			<button @click="react('congrats')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="6" title="%i18n:common.reactions.congrats%"><mk-reaction-icon reaction='congrats'/></button>
+			<button @click="react('angry')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="4" title="%i18n:common.reactions.angry%"><mk-reaction-icon reaction='angry'/></button>
+			<button @click="react('confused')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="5" title="%i18n:common.reactions.confused%"><mk-reaction-icon reaction='confused'/></button>
+			<button @click="react('pudding')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="6" title="%i18n:common.reactions.pudding%"><mk-reaction-icon reaction='pudding'/></button>
+		</div>
+	</div>
+</div>
+</template>
+
+<script>
+	import anime from 'animejs';
+	import api from '../scripts/api';
+
+	export default {
+		props: ['post', 'cb'],
+		methods: {
+			react: function (reaction) {
+				api('posts/reactions/create', {
+					post_id: this.post.id,
+					reaction: reaction
+				}).then(() => {
+					if (this.cb) this.cb();
+					this.$destroy();
+				});
+			}
+		}
+	};
+
+	this.mixin('api');
+
+	this.post = this.opts.post;
+	this.source = this.opts.source;
+
+	const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%';
+
+	this.title = placeholder;
+
+	this.onmouseover = e => {
+		this.update({
+			title: e.target.title
+		});
+	};
+
+	this.onmouseout = () => {
+		this.update({
+			title: placeholder
+		});
+	};
+
+	this.on('mount', () => {
+		const rect = this.source.getBoundingClientRect();
+		const width = this.refs.popover.offsetWidth;
+		const height = this.refs.popover.offsetHeight;
+		if (this.opts.compact) {
+			const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+			const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
+			this.refs.popover.style.left = (x - (width / 2)) + 'px';
+			this.refs.popover.style.top = (y - (height / 2)) + 'px';
+		} else {
+			const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+			const y = rect.top + window.pageYOffset + this.source.offsetHeight;
+			this.refs.popover.style.left = (x - (width / 2)) + 'px';
+			this.refs.popover.style.top = y + 'px';
+		}
+
+		anime({
+			targets: this.refs.backdrop,
+			opacity: 1,
+			duration: 100,
+			easing: 'linear'
+		});
+
+		anime({
+			targets: this.refs.popover,
+			opacity: 1,
+			scale: [0.5, 1],
+			duration: 500
+		});
+	});
+
+	this.react = reaction => {
+
+	};
+
+	this.close = () => {
+		this.refs.backdrop.style.pointerEvents = 'none';
+		anime({
+			targets: this.refs.backdrop,
+			opacity: 0,
+			duration: 200,
+			easing: 'linear'
+		});
+
+		this.refs.popover.style.pointerEvents = 'none';
+		anime({
+			targets: this.refs.popover,
+			opacity: 0,
+			scale: 0.5,
+			duration: 200,
+			easing: 'easeInBack',
+			complete: () => this.unmount()
+		});
+	};
+</script>
+
+<mk-reaction-picker>
+
+	<style>
+		$border-color = rgba(27, 31, 35, 0.15)
+
+		:scope
+			display block
+			position initial
+
+			> .backdrop
+				position fixed
+				top 0
+				left 0
+				z-index 10000
+				width 100%
+				height 100%
+				background rgba(0, 0, 0, 0.1)
+				opacity 0
+
+			> .popover
+				position absolute
+				z-index 10001
+				background #fff
+				border 1px solid $border-color
+				border-radius 4px
+				box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
+				transform scale(0.5)
+				opacity 0
+
+				$balloon-size = 16px
+
+				&:not(.compact)
+					margin-top $balloon-size
+					transform-origin center -($balloon-size)
+
+					&:before
+						content ""
+						display block
+						position absolute
+						top -($balloon-size * 2)
+						left s('calc(50% - %s)', $balloon-size)
+						border-top solid $balloon-size transparent
+						border-left solid $balloon-size transparent
+						border-right solid $balloon-size transparent
+						border-bottom solid $balloon-size $border-color
+
+					&:after
+						content ""
+						display block
+						position absolute
+						top -($balloon-size * 2) + 1.5px
+						left s('calc(50% - %s)', $balloon-size)
+						border-top solid $balloon-size transparent
+						border-left solid $balloon-size transparent
+						border-right solid $balloon-size transparent
+						border-bottom solid $balloon-size #fff
+
+				> p
+					display block
+					margin 0
+					padding 8px 10px
+					font-size 14px
+					color #586069
+					border-bottom solid 1px #e1e4e8
+
+				> div
+					padding 4px
+					width 240px
+					text-align center
+
+					> button
+						width 40px
+						height 40px
+						font-size 24px
+						border-radius 2px
+
+						&:hover
+							background #eee
+
+						&:active
+							background $theme-color
+							box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
+
+	</style>
+
+</mk-reaction-picker>
diff --git a/src/web/app/common/tags/signin-history.tag b/src/web/app/common/tags/signin-history.tag
index cdd58c4c6..10729789c 100644
--- a/src/web/app/common/tags/signin-history.tag
+++ b/src/web/app/common/tags/signin-history.tag
@@ -42,7 +42,7 @@
 </mk-signin-history>
 
 <mk-signin-record>
-	<header onclick={ toggle }>
+	<header @click="toggle">
 		<virtual if={ rec.success }>%fa:check%</virtual>
 		<virtual if={ !rec.success }>%fa:times%</virtual>
 		<span class="ip">{ rec.ip }</span>
diff --git a/src/web/app/common/tags/signup.tag b/src/web/app/common/tags/signup.tag
index b488efb92..d0bd76907 100644
--- a/src/web/app/common/tags/signup.tag
+++ b/src/web/app/common/tags/signup.tag
@@ -36,7 +36,7 @@
 			<input name="agree-tou" type="checkbox" autocomplete="off" required="required"/>
 			<p><a href={ touUrl } target="_blank">利用規約</a>に同意する</p>
 		</label>
-		<button onclick={ onsubmit }>%i18n:common.tags.mk-signup.create%</button>
+		<button @click="onsubmit">%i18n:common.tags.mk-signup.create%</button>
 	</form>
 	<style>
 		:scope
diff --git a/src/web/app/common/tags/stream-indicator.tag b/src/web/app/common/tags/stream-indicator.tag
deleted file mode 100644
index 0eb6196b6..000000000
--- a/src/web/app/common/tags/stream-indicator.tag
+++ /dev/null
@@ -1,78 +0,0 @@
-<mk-stream-indicator>
-	<p if={ connection.state == 'initializing' }>
-		%fa:spinner .pulse%
-		<span>%i18n:common.tags.mk-stream-indicator.connecting%<mk-ellipsis/></span>
-	</p>
-	<p if={ connection.state == 'reconnecting' }>
-		%fa:spinner .pulse%
-		<span>%i18n:common.tags.mk-stream-indicator.reconnecting%<mk-ellipsis/></span>
-	</p>
-	<p if={ connection.state == 'connected' }>
-		%fa:check%
-		<span>%i18n:common.tags.mk-stream-indicator.connected%</span>
-	</p>
-	<style>
-		:scope
-			display block
-			pointer-events none
-			position fixed
-			z-index 16384
-			bottom 8px
-			right 8px
-			margin 0
-			padding 6px 12px
-			font-size 0.9em
-			color #fff
-			background rgba(0, 0, 0, 0.8)
-			border-radius 4px
-
-			> p
-				display block
-				margin 0
-
-				> [data-fa]
-					margin-right 0.25em
-
-	</style>
-	<script>
-		import anime from 'animejs';
-
-		this.mixin('i');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.on('before-mount', () => {
-			if (this.connection.state == 'connected') {
-				this.root.style.opacity = 0;
-			}
-
-			this.connection.on('_connected_', () => {
-				this.update();
-				setTimeout(() => {
-					anime({
-						targets: this.root,
-						opacity: 0,
-						easing: 'linear',
-						duration: 200
-					});
-				}, 1000);
-			});
-
-			this.connection.on('_closed_', () => {
-				this.update();
-				anime({
-					targets: this.root,
-					opacity: 1,
-					easing: 'linear',
-					duration: 100
-				});
-			});
-		});
-
-		this.on('unmount', () => {
-			this.stream.dispose(this.connectionId);
-		});
-	</script>
-</mk-stream-indicator>
diff --git a/src/web/app/common/tags/stream-indicator.vue b/src/web/app/common/tags/stream-indicator.vue
new file mode 100644
index 000000000..619237193
--- /dev/null
+++ b/src/web/app/common/tags/stream-indicator.vue
@@ -0,0 +1,74 @@
+<template>
+	<div>
+		<p v-if=" stream.state == 'initializing' ">
+			%fa:spinner .pulse%
+			<span>%i18n:common.tags.mk-stream-indicator.connecting%<mk-ellipsis/></span>
+		</p>
+		<p v-if=" stream.state == 'reconnecting' ">
+			%fa:spinner .pulse%
+			<span>%i18n:common.tags.mk-stream-indicator.reconnecting%<mk-ellipsis/></span>
+		</p>
+		<p v-if=" stream.state == 'connected' ">
+			%fa:check%
+			<span>%i18n:common.tags.mk-stream-indicator.connected%</span>
+		</p>
+	</div>
+</template>
+
+<script>
+	import anime from 'animejs';
+	import Ellipsis from './ellipsis.vue';
+
+	export default {
+		props: ['stream'],
+		created: function() {
+			if (this.stream.state == 'connected') {
+				this.root.style.opacity = 0;
+			}
+
+			this.stream.on('_connected_', () => {
+				setTimeout(() => {
+					anime({
+						targets: this.root,
+						opacity: 0,
+						easing: 'linear',
+						duration: 200
+					});
+				}, 1000);
+			});
+
+			this.stream.on('_closed_', () => {
+				anime({
+					targets: this.root,
+					opacity: 1,
+					easing: 'linear',
+					duration: 100
+				});
+			});
+		}
+	};
+</script>
+
+<style lang="stylus">
+	> div
+		display block
+		pointer-events none
+		position fixed
+		z-index 16384
+		bottom 8px
+		right 8px
+		margin 0
+		padding 6px 12px
+		font-size 0.9em
+		color #fff
+		background rgba(0, 0, 0, 0.8)
+		border-radius 4px
+
+		> p
+			display block
+			margin 0
+
+			> [data-fa]
+				margin-right 0.25em
+
+</style>
diff --git a/src/web/app/common/tags/twitter-setting.tag b/src/web/app/common/tags/twitter-setting.tag
index 4d57cfa55..8419f8b62 100644
--- a/src/web/app/common/tags/twitter-setting.tag
+++ b/src/web/app/common/tags/twitter-setting.tag
@@ -2,9 +2,9 @@
 	<p>%i18n:common.tags.mk-twitter-setting.description%<a href={ _DOCS_URL_ + '/link-to-twitter' } target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p>
 	<p class="account" if={ I.twitter } title={ 'Twitter ID: ' + I.twitter.user_id }>%i18n:common.tags.mk-twitter-setting.connected-to%: <a href={ 'https://twitter.com/' + I.twitter.screen_name } target="_blank">@{ I.twitter.screen_name }</a></p>
 	<p>
-		<a href={ _API_URL_ + '/connect/twitter' } target="_blank" onclick={ connect }>{ I.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }</a>
+		<a href={ _API_URL_ + '/connect/twitter' } target="_blank" @click="connect">{ I.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }</a>
 		<span if={ I.twitter }> or </span>
-		<a href={ _API_URL_ + '/disconnect/twitter' } target="_blank" if={ I.twitter } onclick={ disconnect }>%i18n:common.tags.mk-twitter-setting.disconnect%</a>
+		<a href={ _API_URL_ + '/disconnect/twitter' } target="_blank" if={ I.twitter } @click="disconnect">%i18n:common.tags.mk-twitter-setting.disconnect%</a>
 	</p>
 	<p class="id" if={ I.twitter }>Twitter ID: { I.twitter.user_id }</p>
 	<style>
diff --git a/src/web/app/desktop/tags/autocomplete-suggestion.tag b/src/web/app/desktop/tags/autocomplete-suggestion.tag
index 731160669..5304875c1 100644
--- a/src/web/app/desktop/tags/autocomplete-suggestion.tag
+++ b/src/web/app/desktop/tags/autocomplete-suggestion.tag
@@ -1,6 +1,6 @@
 <mk-autocomplete-suggestion>
 	<ol class="users" ref="users" if={ users.length > 0 }>
-		<li each={ users } onclick={ parent.onClick } onkeydown={ parent.onKeydown } tabindex="-1">
+		<li each={ users } @click="parent.onClick" onkeydown={ parent.onKeydown } tabindex="-1">
 			<img class="avatar" src={ avatar_url + '?thumbnail&size=32' } alt=""/>
 			<span class="name">{ name }</span>
 			<span class="username">@{ username }</span>
diff --git a/src/web/app/desktop/tags/big-follow-button.tag b/src/web/app/desktop/tags/big-follow-button.tag
index 7634043b2..476f95840 100644
--- a/src/web/app/desktop/tags/big-follow-button.tag
+++ b/src/web/app/desktop/tags/big-follow-button.tag
@@ -1,5 +1,5 @@
 <mk-big-follow-button>
-	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } if={ !init } onclick={ onclick } disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
+	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } if={ !init } @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
 		<span if={ !wait && user.is_following }>%fa:minus%フォロー解除</span>
 		<span if={ !wait && !user.is_following }>%fa:plus%フォロー</span>
 		<virtual if={ wait }>%fa:spinner .pulse .fw%</virtual>
diff --git a/src/web/app/desktop/tags/crop-window.tag b/src/web/app/desktop/tags/crop-window.tag
index 4845b669d..b74b46b77 100644
--- a/src/web/app/desktop/tags/crop-window.tag
+++ b/src/web/app/desktop/tags/crop-window.tag
@@ -4,9 +4,9 @@
 		<yield to="content">
 			<div class="body"><img ref="img" src={ parent.image.url + '?thumbnail&quality=80' } alt=""/></div>
 			<div class="action">
-				<button class="skip" onclick={ parent.skip }>クロップをスキップ</button>
-				<button class="cancel" onclick={ parent.cancel }>キャンセル</button>
-				<button class="ok" onclick={ parent.ok }>決定</button>
+				<button class="skip" @click="parent.skip">クロップをスキップ</button>
+				<button class="cancel" @click="parent.cancel">キャンセル</button>
+				<button class="ok" @click="parent.ok">決定</button>
 			</div>
 		</yield>
 	</mk-window>
diff --git a/src/web/app/desktop/tags/detailed-post-window.tag b/src/web/app/desktop/tags/detailed-post-window.tag
index 04f9acf97..a0bcdc79a 100644
--- a/src/web/app/desktop/tags/detailed-post-window.tag
+++ b/src/web/app/desktop/tags/detailed-post-window.tag
@@ -1,5 +1,5 @@
 <mk-detailed-post-window>
-	<div class="bg" ref="bg" onclick={ bgClick }></div>
+	<div class="bg" ref="bg" @click="bgClick"></div>
 	<div class="main" ref="main" if={ !fetching }>
 		<mk-post-detail ref="detail" post={ post }/>
 	</div>
diff --git a/src/web/app/desktop/tags/dialog.tag b/src/web/app/desktop/tags/dialog.tag
index 743fd6394..f21321173 100644
--- a/src/web/app/desktop/tags/dialog.tag
+++ b/src/web/app/desktop/tags/dialog.tag
@@ -1,11 +1,11 @@
 <mk-dialog>
-	<div class="bg" ref="bg" onclick={ bgClick }></div>
+	<div class="bg" ref="bg" @click="bgClick"></div>
 	<div class="main" ref="main">
 		<header ref="header"></header>
 		<div class="body" ref="body"></div>
 		<div class="buttons">
 			<virtual each={ opts.buttons }>
-				<button onclick={ _onclick }>{ text }</button>
+				<button @click="_onclick">{ text }</button>
 			</virtual>
 		</div>
 	</div>
diff --git a/src/web/app/desktop/tags/donation.tag b/src/web/app/desktop/tags/donation.tag
index 1c19fac1f..b2d18d445 100644
--- a/src/web/app/desktop/tags/donation.tag
+++ b/src/web/app/desktop/tags/donation.tag
@@ -1,5 +1,5 @@
 <mk-donation>
-	<button class="close" onclick={ close }>閉じる x</button>
+	<button class="close" @click="close">閉じる x</button>
 	<div class="message">
 		<p>利用者の皆さま、</p>
 		<p>
diff --git a/src/web/app/desktop/tags/drive/base-contextmenu.tag b/src/web/app/desktop/tags/drive/base-contextmenu.tag
index b16dbf55d..2d7796c68 100644
--- a/src/web/app/desktop/tags/drive/base-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/base-contextmenu.tag
@@ -1,13 +1,13 @@
 <mk-drive-browser-base-contextmenu>
 	<mk-contextmenu ref="ctx">
 		<ul>
-			<li onclick={ parent.createFolder }>
+			<li @click="parent.createFolder">
 				<p>%fa:R folder%%i18n:desktop.tags.mk-drive-browser-base-contextmenu.create-folder%</p>
 			</li>
-			<li onclick={ parent.upload }>
+			<li @click="parent.upload">
 				<p>%fa:upload%%i18n:desktop.tags.mk-drive-browser-base-contextmenu.upload%</p>
 			</li>
-			<li onclick={ parent.urlUpload }>
+			<li @click="parent.urlUpload">
 				<p>%fa:cloud-upload-alt%%i18n:desktop.tags.mk-drive-browser-base-contextmenu.url-upload%</p>
 			</li>
 		</ul>
diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/tags/drive/browser.tag
index a60a46b79..f9dea5127 100644
--- a/src/web/app/desktop/tags/drive/browser.tag
+++ b/src/web/app/desktop/tags/drive/browser.tag
@@ -28,7 +28,7 @@
 				</virtual>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 				<div class="padding" each={ Array(10).fill(16) }></div>
-				<button if={ moreFiles } onclick={ fetchMoreFiles }>%i18n:desktop.tags.mk-drive-browser.load-more%</button>
+				<button if={ moreFiles } @click="fetchMoreFiles">%i18n:desktop.tags.mk-drive-browser.load-more%</button>
 			</div>
 			<div class="empty" if={ files.length == 0 && folders.length == 0 && !fetching }>
 				<p if={ draghover }>%i18n:desktop.tags.mk-drive-browser.empty-draghover%</p>
diff --git a/src/web/app/desktop/tags/drive/file-contextmenu.tag b/src/web/app/desktop/tags/drive/file-contextmenu.tag
index 532417c75..31ab05c23 100644
--- a/src/web/app/desktop/tags/drive/file-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/file-contextmenu.tag
@@ -1,25 +1,25 @@
 <mk-drive-browser-file-contextmenu>
 	<mk-contextmenu ref="ctx">
 		<ul>
-			<li onclick={ parent.rename }>
+			<li @click="parent.rename">
 				<p>%fa:i-cursor%%i18n:desktop.tags.mk-drive-browser-file-contextmenu.rename%</p>
 			</li>
-			<li onclick={ parent.copyUrl }>
+			<li @click="parent.copyUrl">
 				<p>%fa:link%%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copy-url%</p>
 			</li>
-			<li><a href={ parent.file.url + '?download' } download={ parent.file.name } onclick={ parent.download }>%fa:download%%i18n:desktop.tags.mk-drive-browser-file-contextmenu.download%</a></li>
+			<li><a href={ parent.file.url + '?download' } download={ parent.file.name } @click="parent.download">%fa:download%%i18n:desktop.tags.mk-drive-browser-file-contextmenu.download%</a></li>
 			<li class="separator"></li>
-			<li onclick={ parent.delete }>
+			<li @click="parent.delete">
 				<p>%fa:R trash-alt%%i18n:common.delete%</p>
 			</li>
 			<li class="separator"></li>
 			<li class="has-child">
 				<p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.else-files%%fa:caret-right%</p>
 				<ul>
-					<li onclick={ parent.setAvatar }>
+					<li @click="parent.setAvatar">
 						<p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.set-as-avatar%</p>
 					</li>
-					<li onclick={ parent.setBanner }>
+					<li @click="parent.setBanner">
 						<p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.set-as-banner%</p>
 					</li>
 				</ul>
@@ -27,7 +27,7 @@
 			<li class="has-child">
 				<p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.open-in-app%...%fa:caret-right%</p>
 				<ul>
-					<li onclick={ parent.addApp }>
+					<li @click="parent.addApp">
 						<p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.add-app%...</p>
 					</li>
 				</ul>
diff --git a/src/web/app/desktop/tags/drive/file.tag b/src/web/app/desktop/tags/drive/file.tag
index 8b3d36b3f..2a1519dc7 100644
--- a/src/web/app/desktop/tags/drive/file.tag
+++ b/src/web/app/desktop/tags/drive/file.tag
@@ -1,4 +1,4 @@
-<mk-drive-browser-file data-is-selected={ isSelected } data-is-contextmenu-showing={ isContextmenuShowing.toString() } onclick={ onclick } oncontextmenu={ oncontextmenu } draggable="true" ondragstart={ ondragstart } ondragend={ ondragend } title={ title }>
+<mk-drive-browser-file data-is-selected={ isSelected } data-is-contextmenu-showing={ isContextmenuShowing.toString() } @click="onclick" oncontextmenu={ oncontextmenu } draggable="true" ondragstart={ ondragstart } ondragend={ ondragend } title={ title }>
 	<div class="label" if={ I.avatar_id == file.id }><img src="/assets/label.svg"/>
 		<p>%i18n:desktop.tags.mk-drive-browser-file.avatar%</p>
 	</div>
diff --git a/src/web/app/desktop/tags/drive/folder-contextmenu.tag b/src/web/app/desktop/tags/drive/folder-contextmenu.tag
index c6a1ea3b8..eb8cad52a 100644
--- a/src/web/app/desktop/tags/drive/folder-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/folder-contextmenu.tag
@@ -1,18 +1,18 @@
 <mk-drive-browser-folder-contextmenu>
 	<mk-contextmenu ref="ctx">
 		<ul>
-			<li onclick={ parent.move }>
+			<li @click="parent.move">
 				<p>%fa:arrow-right%%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.move-to-this-folder%</p>
 			</li>
-			<li onclick={ parent.newWindow }>
+			<li @click="parent.newWindow">
 				<p>%fa:R window-restore%%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.show-in-new-window%</p>
 			</li>
 			<li class="separator"></li>
-			<li onclick={ parent.rename }>
+			<li @click="parent.rename">
 				<p>%fa:i-cursor%%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.rename%</p>
 			</li>
 			<li class="separator"></li>
-			<li onclick={ parent.delete }>
+			<li @click="parent.delete">
 				<p>%fa:R trash-alt%%i18n:common.delete%</p>
 			</li>
 		</ul>
diff --git a/src/web/app/desktop/tags/drive/folder.tag b/src/web/app/desktop/tags/drive/folder.tag
index 0b7ee6e2d..2fae55e50 100644
--- a/src/web/app/desktop/tags/drive/folder.tag
+++ b/src/web/app/desktop/tags/drive/folder.tag
@@ -1,4 +1,4 @@
-<mk-drive-browser-folder data-is-contextmenu-showing={ isContextmenuShowing.toString() } data-draghover={ draghover.toString() } onclick={ onclick } onmouseover={ onmouseover } onmouseout={ onmouseout } ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop } oncontextmenu={ oncontextmenu } draggable="true" ondragstart={ ondragstart } ondragend={ ondragend } title={ title }>
+<mk-drive-browser-folder data-is-contextmenu-showing={ isContextmenuShowing.toString() } data-draghover={ draghover.toString() } @click="onclick" onmouseover={ onmouseover } onmouseout={ onmouseout } ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop } oncontextmenu={ oncontextmenu } draggable="true" ondragstart={ ondragstart } ondragend={ ondragend } title={ title }>
 	<p class="name"><virtual if={ hover }>%fa:R folder-open .fw%</virtual><virtual if={ !hover }>%fa:R folder .fw%</virtual>{ folder.name }</p>
 	<style>
 		:scope
diff --git a/src/web/app/desktop/tags/drive/nav-folder.tag b/src/web/app/desktop/tags/drive/nav-folder.tag
index 43a648b52..d688d2e08 100644
--- a/src/web/app/desktop/tags/drive/nav-folder.tag
+++ b/src/web/app/desktop/tags/drive/nav-folder.tag
@@ -1,4 +1,4 @@
-<mk-drive-browser-nav-folder data-draghover={ draghover } onclick={ onclick } ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop }>
+<mk-drive-browser-nav-folder data-draghover={ draghover } @click="onclick" ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop }>
 	<virtual if={ folder == null }>%fa:cloud%</virtual><span>{ folder == null ? '%i18n:desktop.tags.mk-drive-browser-nav-folder.drive%' : folder.name }</span>
 	<style>
 		:scope
diff --git a/src/web/app/desktop/tags/follow-button.tag b/src/web/app/desktop/tags/follow-button.tag
index ce6de3ac6..8a1f7b2c1 100644
--- a/src/web/app/desktop/tags/follow-button.tag
+++ b/src/web/app/desktop/tags/follow-button.tag
@@ -1,5 +1,5 @@
 <mk-follow-button>
-	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } if={ !init } onclick={ onclick } disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
+	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } if={ !init } @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
 		<virtual if={ !wait && user.is_following }>%fa:minus%</virtual>
 		<virtual if={ !wait && !user.is_following }>%fa:plus%</virtual>
 		<virtual if={ wait }>%fa:spinner .pulse .fw%</virtual>
diff --git a/src/web/app/desktop/tags/following-setuper.tag b/src/web/app/desktop/tags/following-setuper.tag
index a51a38ccd..828098629 100644
--- a/src/web/app/desktop/tags/following-setuper.tag
+++ b/src/web/app/desktop/tags/following-setuper.tag
@@ -10,8 +10,8 @@
 	</div>
 	<p class="empty" if={ !fetching && users.length == 0 }>おすすめのユーザーは見つかりませんでした。</p>
 	<p class="fetching" if={ fetching }>%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
-	<a class="refresh" onclick={ refresh }>もっと見る</a>
-	<button class="close" onclick={ close } title="閉じる">%fa:times%</button>
+	<a class="refresh" @click="refresh">もっと見る</a>
+	<button class="close" @click="close" title="閉じる">%fa:times%</button>
 	<style>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/home-widgets/broadcast.tag b/src/web/app/desktop/tags/home-widgets/broadcast.tag
index 6f4bb0756..157c42963 100644
--- a/src/web/app/desktop/tags/home-widgets/broadcast.tag
+++ b/src/web/app/desktop/tags/home-widgets/broadcast.tag
@@ -13,7 +13,7 @@
 		broadcasts.length == 0 ? '%i18n:desktop.tags.mk-broadcast-home-widget.no-broadcasts%' : broadcasts[i].title
 	}</h1>
 	<p if={ !fetching }><mk-raw if={ broadcasts.length != 0 } content={ broadcasts[i].text }/><virtual if={ broadcasts.length == 0 }>%i18n:desktop.tags.mk-broadcast-home-widget.have-a-nice-day%</virtual></p>
-	<a if={ broadcasts.length > 1 } onclick={ next }>%i18n:desktop.tags.mk-broadcast-home-widget.next% &gt;&gt;</a>
+	<a if={ broadcasts.length > 1 } @click="next">%i18n:desktop.tags.mk-broadcast-home-widget.next% &gt;&gt;</a>
 	<style>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/home-widgets/channel.tag b/src/web/app/desktop/tags/home-widgets/channel.tag
index 545bc38ac..0e40caa1e 100644
--- a/src/web/app/desktop/tags/home-widgets/channel.tag
+++ b/src/web/app/desktop/tags/home-widgets/channel.tag
@@ -3,7 +3,7 @@
 		<p class="title">%fa:tv%{
 			channel ? channel.title : '%i18n:desktop.tags.mk-channel-home-widget.title%'
 		}</p>
-		<button onclick={ settings } title="%i18n:desktop.tags.mk-channel-home-widget.settings%">%fa:cog%</button>
+		<button @click="settings" title="%i18n:desktop.tags.mk-channel-home-widget.settings%">%fa:cog%</button>
 	</virtual>
 	<p class="get-started" if={ this.data.channel == null }>%i18n:desktop.tags.mk-channel-home-widget.get-started%</p>
 	<mk-channel ref="channel" show={ this.data.channel }/>
@@ -192,7 +192,7 @@
 
 <mk-channel-post>
 	<header>
-		<a class="index" onclick={ reply }>{ post.index }:</a>
+		<a class="index" @click="reply">{ post.index }:</a>
 		<a class="name" href={ _URL_ + '/' + post.user.username }><b>{ post.user.name }</b></a>
 		<span>ID:<i>{ post.user.username }</i></span>
 	</header>
diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/tags/home-widgets/mentions.tag
index 268728307..94329f030 100644
--- a/src/web/app/desktop/tags/home-widgets/mentions.tag
+++ b/src/web/app/desktop/tags/home-widgets/mentions.tag
@@ -1,5 +1,5 @@
 <mk-mentions-home-widget>
-	<header><span data-is-active={ mode == 'all' } onclick={ setMode.bind(this, 'all') }>すべて</span><span data-is-active={ mode == 'following' } onclick={ setMode.bind(this, 'following') }>フォロー中</span></header>
+	<header><span data-is-active={ mode == 'all' } @click="setMode.bind(this, 'all')">すべて</span><span data-is-active={ mode == 'following' } @click="setMode.bind(this, 'following')">フォロー中</span></header>
 	<div class="loading" if={ isLoading }>
 		<mk-ellipsis-icon/>
 	</div>
diff --git a/src/web/app/desktop/tags/home-widgets/notifications.tag b/src/web/app/desktop/tags/home-widgets/notifications.tag
index 0ccd832d7..051714eab 100644
--- a/src/web/app/desktop/tags/home-widgets/notifications.tag
+++ b/src/web/app/desktop/tags/home-widgets/notifications.tag
@@ -1,7 +1,7 @@
 <mk-notifications-home-widget>
 	<virtual if={ !data.compact }>
 		<p class="title">%fa:R bell%%i18n:desktop.tags.mk-notifications-home-widget.title%</p>
-		<button onclick={ settings } title="%i18n:desktop.tags.mk-notifications-home-widget.settings%">%fa:cog%</button>
+		<button @click="settings" title="%i18n:desktop.tags.mk-notifications-home-widget.settings%">%fa:cog%</button>
 	</virtual>
 	<mk-notifications/>
 	<style>
diff --git a/src/web/app/desktop/tags/home-widgets/post-form.tag b/src/web/app/desktop/tags/home-widgets/post-form.tag
index c8ccc5a30..b6310d6aa 100644
--- a/src/web/app/desktop/tags/home-widgets/post-form.tag
+++ b/src/web/app/desktop/tags/home-widgets/post-form.tag
@@ -5,7 +5,7 @@
 			<p class="title">%fa:pencil-alt%%i18n:desktop.tags.mk-post-form-home-widget.title%</p>
 		</virtual>
 		<textarea disabled={ posting } ref="text" onkeydown={ onkeydown } placeholder="%i18n:desktop.tags.mk-post-form-home-widget.placeholder%"></textarea>
-		<button onclick={ post } disabled={ posting }>%i18n:desktop.tags.mk-post-form-home-widget.post%</button>
+		<button @click="post" disabled={ posting }>%i18n:desktop.tags.mk-post-form-home-widget.post%</button>
 	</virtual>
 	<style>
 		:scope
diff --git a/src/web/app/desktop/tags/home-widgets/profile.tag b/src/web/app/desktop/tags/home-widgets/profile.tag
index eb8ba52e8..bba5b0c47 100644
--- a/src/web/app/desktop/tags/home-widgets/profile.tag
+++ b/src/web/app/desktop/tags/home-widgets/profile.tag
@@ -1,6 +1,6 @@
 <mk-profile-home-widget data-compact={ data.design == 1 || data.design == 2 } data-melt={ data.design == 2 }>
-	<div class="banner" style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=256)' : '' } title="クリックでバナー編集" onclick={ setBanner }></div>
-	<img class="avatar" src={ I.avatar_url + '?thumbnail&size=96' } onclick={ setAvatar } alt="avatar" title="クリックでアバター編集" data-user-preview={ I.id }/>
+	<div class="banner" style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=256)' : '' } title="クリックでバナー編集" @click="setBanner"></div>
+	<img class="avatar" src={ I.avatar_url + '?thumbnail&size=96' } @click="setAvatar" alt="avatar" title="クリックでアバター編集" data-user-preview={ I.id }/>
 	<a class="name" href={ '/' + I.username }>{ I.name }</a>
 	<p class="username">@{ I.username }</p>
 	<style>
diff --git a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag b/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
index 776f66601..5489edf5f 100644
--- a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
+++ b/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
@@ -1,7 +1,7 @@
 <mk-recommended-polls-home-widget>
 	<virtual if={ !data.compact }>
 		<p class="title">%fa:chart-pie%%i18n:desktop.tags.mk-recommended-polls-home-widget.title%</p>
-		<button onclick={ fetch } title="%i18n:desktop.tags.mk-recommended-polls-home-widget.refresh%">%fa:sync%</button>
+		<button @click="fetch" title="%i18n:desktop.tags.mk-recommended-polls-home-widget.refresh%">%fa:sync%</button>
 	</virtual>
 	<div class="poll" if={ !loading && poll != null }>
 		<p if={ poll.text }><a href="/{ poll.user.username }/{ poll.id }">{ poll.text }</a></p>
diff --git a/src/web/app/desktop/tags/home-widgets/rss-reader.tag b/src/web/app/desktop/tags/home-widgets/rss-reader.tag
index a927693ce..45cc62a51 100644
--- a/src/web/app/desktop/tags/home-widgets/rss-reader.tag
+++ b/src/web/app/desktop/tags/home-widgets/rss-reader.tag
@@ -1,7 +1,7 @@
 <mk-rss-reader-home-widget>
 	<virtual if={ !data.compact }>
 		<p class="title">%fa:rss-square%RSS</p>
-		<button onclick={ settings } title="設定">%fa:cog%</button>
+		<button @click="settings" title="設定">%fa:cog%</button>
 	</virtual>
 	<div class="feed" if={ !initializing }>
 		<virtual each={ item in items }><a href={ item.link } target="_blank">{ item.title }</a></virtual>
diff --git a/src/web/app/desktop/tags/home-widgets/server.tag b/src/web/app/desktop/tags/home-widgets/server.tag
index b9b191c18..6eb4ce15b 100644
--- a/src/web/app/desktop/tags/home-widgets/server.tag
+++ b/src/web/app/desktop/tags/home-widgets/server.tag
@@ -1,7 +1,7 @@
 <mk-server-home-widget data-melt={ data.design == 2 }>
 	<virtual if={ data.design == 0 }>
 		<p class="title">%fa:server%%i18n:desktop.tags.mk-server-home-widget.title%</p>
-		<button onclick={ toggle } title="%i18n:desktop.tags.mk-server-home-widget.toggle%">%fa:sort%</button>
+		<button @click="toggle" title="%i18n:desktop.tags.mk-server-home-widget.toggle%">%fa:sort%</button>
 	</virtual>
 	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<mk-server-home-widget-cpu-and-memory-usage if={ !initializing } show={ data.view == 0 } connection={ connection }/>
diff --git a/src/web/app/desktop/tags/home-widgets/slideshow.tag b/src/web/app/desktop/tags/home-widgets/slideshow.tag
index 53fe04700..af54fd893 100644
--- a/src/web/app/desktop/tags/home-widgets/slideshow.tag
+++ b/src/web/app/desktop/tags/home-widgets/slideshow.tag
@@ -1,11 +1,11 @@
 <mk-slideshow-home-widget>
-	<div onclick={ choose }>
+	<div @click="choose">
 		<p if={ data.folder === undefined }>クリックしてフォルダを指定してください</p>
 		<p if={ data.folder !== undefined && images.length == 0 && !fetching }>このフォルダには画像がありません</p>
 		<div ref="slideA" class="slide a"></div>
 		<div ref="slideB" class="slide b"></div>
 	</div>
-	<button onclick={ resize }>%fa:expand%</button>
+	<button @click="resize">%fa:expand%</button>
 	<style>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/home-widgets/trends.tag b/src/web/app/desktop/tags/home-widgets/trends.tag
index 3a2304111..637f53a60 100644
--- a/src/web/app/desktop/tags/home-widgets/trends.tag
+++ b/src/web/app/desktop/tags/home-widgets/trends.tag
@@ -1,7 +1,7 @@
 <mk-trends-home-widget>
 	<virtual if={ !data.compact }>
 		<p class="title">%fa:fire%%i18n:desktop.tags.mk-trends-home-widget.title%</p>
-		<button onclick={ fetch } title="%i18n:desktop.tags.mk-trends-home-widget.refresh%">%fa:sync%</button>
+		<button @click="fetch" title="%i18n:desktop.tags.mk-trends-home-widget.refresh%">%fa:sync%</button>
 	</virtual>
 	<div class="post" if={ !loading && post != null }>
 		<p class="text"><a href="/{ post.user.username }/{ post.id }">{ post.text }</a></p>
diff --git a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
index a1af7a5c4..881373f8d 100644
--- a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
+++ b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
@@ -1,7 +1,7 @@
 <mk-user-recommendation-home-widget>
 	<virtual if={ !data.compact }>
 		<p class="title">%fa:users%%i18n:desktop.tags.mk-user-recommendation-home-widget.title%</p>
-		<button onclick={ refresh } title="%i18n:desktop.tags.mk-user-recommendation-home-widget.refresh%">%fa:sync%</button>
+		<button @click="refresh" title="%i18n:desktop.tags.mk-user-recommendation-home-widget.refresh%">%fa:sync%</button>
 	</virtual>
 	<div class="user" if={ !loading && users.length != 0 } each={ _user in users }>
 		<a class="avatar-anchor" href={ '/' + _user.username }>
diff --git a/src/web/app/desktop/tags/home.tag b/src/web/app/desktop/tags/home.tag
index 50f6c8460..90486f7d2 100644
--- a/src/web/app/desktop/tags/home.tag
+++ b/src/web/app/desktop/tags/home.tag
@@ -27,7 +27,7 @@
 					<option value="nav">ナビゲーション</option>
 					<option value="tips">ヒント</option>
 				</select>
-				<button onclick={ addWidget }>追加</button>
+				<button @click="addWidget">追加</button>
 			</div>
 			<div class="trash">
 				<div ref="trash"></div>
diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
index 0cd408576..1c81af3d0 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -58,7 +58,7 @@
 		onmousemove={ mousemove }
 		onmouseleave={ mouseleave }
 		style={ styles }
-		onclick={ click }
+		@click="click"
 		title={ image.name }></a>
 	<style>
 		:scope
@@ -110,7 +110,7 @@
 </mk-images-image>
 
 <mk-image-dialog>
-	<div class="bg" ref="bg" onclick={ close }></div><img ref="img" src={ image.url } alt={ image.name } title={ image.name } onclick={ close }/>
+	<div class="bg" ref="bg" @click="close"></div><img ref="img" src={ image.url } alt={ image.name } title={ image.name } @click="close"/>
 	<style>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/input-dialog.tag b/src/web/app/desktop/tags/input-dialog.tag
index f17527754..84dcedf93 100644
--- a/src/web/app/desktop/tags/input-dialog.tag
+++ b/src/web/app/desktop/tags/input-dialog.tag
@@ -8,8 +8,8 @@
 				<input ref="text" type={ parent.type } oninput={ parent.onInput } onkeydown={ parent.onKeydown } placeholder={ parent.placeholder }/>
 			</div>
 			<div class="action">
-				<button class="cancel" onclick={ parent.cancel }>キャンセル</button>
-				<button class="ok" disabled={ !parent.allowEmpty && refs.text.value.length == 0 } onclick={ parent.ok }>決定</button>
+				<button class="cancel" @click="parent.cancel">キャンセル</button>
+				<button class="ok" disabled={ !parent.allowEmpty && refs.text.value.length == 0 } @click="parent.ok">決定</button>
 			</div>
 		</yield>
 	</mk-window>
diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag
index 39862487e..91876c24f 100644
--- a/src/web/app/desktop/tags/notifications.tag
+++ b/src/web/app/desktop/tags/notifications.tag
@@ -78,7 +78,7 @@
 			</p>
 		</virtual>
 	</div>
-	<button class="more { fetching: fetchingMoreNotifications }" if={ moreNotifications } onclick={ fetchMoreNotifications } disabled={ fetchingMoreNotifications }>
+	<button class="more { fetching: fetchingMoreNotifications }" if={ moreNotifications } @click="fetchMoreNotifications" disabled={ fetchingMoreNotifications }>
 		<virtual if={ fetchingMoreNotifications }>%fa:spinner .pulse .fw%</virtual>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:desktop.tags.mk-notifications.more%' }
 	</button>
 	<p class="empty" if={ notifications.length == 0 && !loading }>ありません!</p>
diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/tags/pages/entrance.tag
index 974f49a4f..d3807a1e7 100644
--- a/src/web/app/desktop/tags/pages/entrance.tag
+++ b/src/web/app/desktop/tags/pages/entrance.tag
@@ -10,7 +10,7 @@
 			<mk-entrance-signup if={ mode == 'signup' }/>
 			<div class="introduction" if={ mode == 'introduction' }>
 				<mk-introduction/>
-				<button onclick={ signin }>わかった</button>
+				<button @click="signin">わかった</button>
 			</div>
 		</div>
 	</main>
@@ -159,7 +159,7 @@
 	</div>
 	<a href={ _API_URL_ + '/signin/twitter' }>Twitterでサインイン</a>
 	<div class="divider"><span>or</span></div>
-	<button class="signup" onclick={ parent.signup }>新規登録</button><a class="introduction" onclick={ introduction }>Misskeyについて</a>
+	<button class="signup" @click="parent.signup">新規登録</button><a class="introduction" @click="introduction">Misskeyについて</a>
 	<style>
 		:scope
 			display block
@@ -295,7 +295,7 @@
 
 <mk-entrance-signup>
 	<mk-signup/>
-	<button class="cancel" type="button" onclick={ parent.signin } title="キャンセル">%fa:times%</button>
+	<button class="cancel" type="button" @click="parent.signin" title="キャンセル">%fa:times%</button>
 	<style>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/pages/selectdrive.tag b/src/web/app/desktop/tags/pages/selectdrive.tag
index 123977e90..993df680f 100644
--- a/src/web/app/desktop/tags/pages/selectdrive.tag
+++ b/src/web/app/desktop/tags/pages/selectdrive.tag
@@ -1,9 +1,9 @@
 <mk-selectdrive-page>
 	<mk-drive-browser ref="browser" multiple={ multiple }/>
 	<div>
-		<button class="upload" title="%i18n:desktop.tags.mk-selectdrive-page.upload%" onclick={ upload }>%fa:upload%</button>
-		<button class="cancel" onclick={ close }>%i18n:desktop.tags.mk-selectdrive-page.cancel%</button>
-		<button class="ok" onclick={ ok }>%i18n:desktop.tags.mk-selectdrive-page.ok%</button>
+		<button class="upload" title="%i18n:desktop.tags.mk-selectdrive-page.upload%" @click="upload">%fa:upload%</button>
+		<button class="cancel" @click="close">%i18n:desktop.tags.mk-selectdrive-page.cancel%</button>
+		<button class="ok" @click="ok">%i18n:desktop.tags.mk-selectdrive-page.ok%</button>
 	</div>
 
 	<style>
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index 47c71a6c1..6177f24ee 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -1,6 +1,6 @@
 <mk-post-detail title={ title }>
 	<div class="main">
-		<button class="read-more" if={ p.reply && p.reply.reply_id && context == null } title="会話をもっと読み込む" onclick={ loadContext } disabled={ contextFetching }>
+		<button class="read-more" if={ p.reply && p.reply.reply_id && context == null } title="会話をもっと読み込む" @click="loadContext" disabled={ contextFetching }>
 			<virtual if={ !contextFetching }>%fa:ellipsis-v%</virtual>
 			<virtual if={ contextFetching }>%fa:spinner .pulse%</virtual>
 		</button>
@@ -43,16 +43,16 @@
 			</div>
 			<footer>
 				<mk-reactions-viewer post={ p }/>
-				<button onclick={ reply } title="返信">
+				<button @click="reply" title="返信">
 					%fa:reply%<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
 				</button>
-				<button onclick={ repost } title="Repost">
+				<button @click="repost" title="Repost">
 					%fa:retweet%<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
 				</button>
-				<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="リアクション">
+				<button class={ reacted: p.my_reaction != null } @click="react" ref="reactButton" title="リアクション">
 					%fa:plus%<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
 				</button>
-				<button onclick={ menu } ref="menuButton">
+				<button @click="menu" ref="menuButton">
 					%fa:ellipsis-h%
 				</button>
 			</footer>
diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag
index 0b4c07906..23434a824 100644
--- a/src/web/app/desktop/tags/post-form.tag
+++ b/src/web/app/desktop/tags/post-form.tag
@@ -5,7 +5,7 @@
 			<ul ref="media">
 				<li each={ files } data-id={ id }>
 					<div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name }></div>
-					<img class="remove" onclick={ removeFile } src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/>
+					<img class="remove" @click="removeFile" src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/>
 				</li>
 			</ul>
 			<p class="remain">{ 4 - files.length }/4</p>
@@ -13,12 +13,12 @@
 		<mk-poll-editor if={ poll } ref="poll" ondestroy={ onPollDestroyed }/>
 	</div>
 	<mk-uploader ref="uploader"/>
-	<button ref="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" onclick={ selectFile }>%fa:upload%</button>
-	<button ref="drive" title="%i18n:desktop.tags.mk-post-form.attach-media-from-drive%" onclick={ selectFileFromDrive }>%fa:cloud%</button>
-	<button class="kao" title="%i18n:desktop.tags.mk-post-form.insert-a-kao%" onclick={ kao }>%fa:R smile%</button>
-	<button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" onclick={ addPoll }>%fa:chart-pie%</button>
+	<button ref="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" @click="selectFile">%fa:upload%</button>
+	<button ref="drive" title="%i18n:desktop.tags.mk-post-form.attach-media-from-drive%" @click="selectFileFromDrive">%fa:cloud%</button>
+	<button class="kao" title="%i18n:desktop.tags.mk-post-form.insert-a-kao%" @click="kao">%fa:R smile%</button>
+	<button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" @click="addPoll">%fa:chart-pie%</button>
 	<p class="text-count { over: refs.text.value.length > 1000 }">{ '%i18n:desktop.tags.mk-post-form.text-remain%'.replace('{}', 1000 - refs.text.value.length) }</p>
-	<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0 && files.length == 0 && !poll && !repost) } onclick={ post }>
+	<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0 && files.length == 0 && !poll && !repost) } @click="post">
 		{ wait ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }<mk-ellipsis if={ wait }/>
 	</button>
 	<input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" onchange={ changeFile }/>
diff --git a/src/web/app/desktop/tags/repost-form.tag b/src/web/app/desktop/tags/repost-form.tag
index c3cf6c1fb..946871765 100644
--- a/src/web/app/desktop/tags/repost-form.tag
+++ b/src/web/app/desktop/tags/repost-form.tag
@@ -2,9 +2,9 @@
 	<mk-post-preview post={ opts.post }/>
 	<virtual if={ !quote }>
 		<footer>
-			<a class="quote" if={ !quote } onclick={ onquote }>%i18n:desktop.tags.mk-repost-form.quote%</a>
-			<button class="cancel" onclick={ cancel }>%i18n:desktop.tags.mk-repost-form.cancel%</button>
-			<button class="ok" onclick={ ok } disabled={ wait }>{ wait ? '%i18n:desktop.tags.mk-repost-form.reposting%' : '%i18n:desktop.tags.mk-repost-form.repost%' }</button>
+			<a class="quote" if={ !quote } @click="onquote">%i18n:desktop.tags.mk-repost-form.quote%</a>
+			<button class="cancel" @click="cancel">%i18n:desktop.tags.mk-repost-form.cancel%</button>
+			<button class="ok" @click="ok" disabled={ wait }>{ wait ? '%i18n:desktop.tags.mk-repost-form.reposting%' : '%i18n:desktop.tags.mk-repost-form.repost%' }</button>
 		</footer>
 	</virtual>
 	<virtual if={ quote }>
diff --git a/src/web/app/desktop/tags/select-file-from-drive-window.tag b/src/web/app/desktop/tags/select-file-from-drive-window.tag
index c660a2fe9..622514558 100644
--- a/src/web/app/desktop/tags/select-file-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-file-from-drive-window.tag
@@ -7,9 +7,9 @@
 		<yield to="content">
 			<mk-drive-browser ref="browser" multiple={ parent.multiple }/>
 			<div>
-				<button class="upload" title="PCからドライブにファイルをアップロード" onclick={ parent.upload }>%fa:upload%</button>
-				<button class="cancel" onclick={ parent.close }>キャンセル</button>
-				<button class="ok" disabled={ parent.multiple && parent.files.length == 0 } onclick={ parent.ok }>決定</button>
+				<button class="upload" title="PCからドライブにファイルをアップロード" @click="parent.upload">%fa:upload%</button>
+				<button class="cancel" @click="parent.close">キャンセル</button>
+				<button class="ok" disabled={ parent.multiple && parent.files.length == 0 } @click="parent.ok">決定</button>
 			</div>
 		</yield>
 	</mk-window>
diff --git a/src/web/app/desktop/tags/select-folder-from-drive-window.tag b/src/web/app/desktop/tags/select-folder-from-drive-window.tag
index 3c66a4e6d..45700420c 100644
--- a/src/web/app/desktop/tags/select-folder-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-folder-from-drive-window.tag
@@ -6,8 +6,8 @@
 		<yield to="content">
 			<mk-drive-browser ref="browser"/>
 			<div>
-				<button class="cancel" onclick={ parent.close }>キャンセル</button>
-				<button class="ok" onclick={ parent.ok }>決定</button>
+				<button class="cancel" @click="parent.close">キャンセル</button>
+				<button class="ok" @click="parent.ok">決定</button>
 			</div>
 		</yield>
 	</mk-window>
diff --git a/src/web/app/desktop/tags/set-avatar-suggestion.tag b/src/web/app/desktop/tags/set-avatar-suggestion.tag
index 7e871129f..faf4cdd8a 100644
--- a/src/web/app/desktop/tags/set-avatar-suggestion.tag
+++ b/src/web/app/desktop/tags/set-avatar-suggestion.tag
@@ -1,6 +1,6 @@
-<mk-set-avatar-suggestion onclick={ set }>
+<mk-set-avatar-suggestion @click="set">
 	<p><b>アバターを設定</b>してみませんか?
-		<button onclick={ close }>%fa:times%</button>
+		<button @click="close">%fa:times%</button>
 	</p>
 	<style>
 		:scope
diff --git a/src/web/app/desktop/tags/set-banner-suggestion.tag b/src/web/app/desktop/tags/set-banner-suggestion.tag
index 4cd364ca3..cbf0f1b68 100644
--- a/src/web/app/desktop/tags/set-banner-suggestion.tag
+++ b/src/web/app/desktop/tags/set-banner-suggestion.tag
@@ -1,6 +1,6 @@
-<mk-set-banner-suggestion onclick={ set }>
+<mk-set-banner-suggestion @click="set">
 	<p><b>バナーを設定</b>してみませんか?
-		<button onclick={ close }>%fa:times%</button>
+		<button @click="close">%fa:times%</button>
 	</p>
 	<style>
 		:scope
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 457b7e227..efc5da83f 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -131,7 +131,7 @@
 <mk-profile-setting>
 	<label class="avatar ui from group">
 		<p>%i18n:desktop.tags.mk-profile-setting.avatar%</p><img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		<button class="ui" onclick={ avatar }>%i18n:desktop.tags.mk-profile-setting.choice-avatar%</button>
+		<button class="ui" @click="avatar">%i18n:desktop.tags.mk-profile-setting.choice-avatar%</button>
 	</label>
 	<label class="ui from group">
 		<p>%i18n:desktop.tags.mk-profile-setting.name%</p>
@@ -149,7 +149,7 @@
 		<p>%i18n:desktop.tags.mk-profile-setting.birthday%</p>
 		<input ref="accountBirthday" type="date" value={ I.profile.birthday } class="ui"/>
 	</label>
-	<button class="ui primary" onclick={ updateAccount }>%i18n:desktop.tags.mk-profile-setting.save%</button>
+	<button class="ui primary" @click="updateAccount">%i18n:desktop.tags.mk-profile-setting.save%</button>
 	<style>
 		:scope
 			display block
@@ -195,7 +195,7 @@
 	<p>%i18n:desktop.tags.mk-api-info.intro%</p>
 	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-api-info.caution%</p></div>
 	<p>%i18n:desktop.tags.mk-api-info.regeneration-of-token%</p>
-	<button class="ui" onclick={ regenerateToken }>%i18n:desktop.tags.mk-api-info.regenerate-token%</button>
+	<button class="ui" @click="regenerateToken">%i18n:desktop.tags.mk-api-info.regenerate-token%</button>
 	<style>
 		:scope
 			display block
@@ -225,7 +225,7 @@
 </mk-api-info>
 
 <mk-password-setting>
-	<button onclick={ reset } class="ui primary">%i18n:desktop.tags.mk-password-setting.reset%</button>
+	<button @click="reset" class="ui primary">%i18n:desktop.tags.mk-password-setting.reset%</button>
 	<style>
 		:scope
 			display block
@@ -265,10 +265,10 @@
 <mk-2fa-setting>
 	<p>%i18n:desktop.tags.mk-2fa-setting.intro%<a href="%i18n:desktop.tags.mk-2fa-setting.url%" target="_blank">%i18n:desktop.tags.mk-2fa-setting.detail%</a></p>
 	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-2fa-setting.caution%</p></div>
-	<p if={ !data && !I.two_factor_enabled }><button onclick={ register } class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
+	<p if={ !data && !I.two_factor_enabled }><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
 	<virtual if={ I.two_factor_enabled }>
 		<p>%i18n:desktop.tags.mk-2fa-setting.already-registered%</p>
-		<button onclick={ unregister } class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button>
+		<button @click="unregister" class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button>
 	</virtual>
 	<div if={ data }>
 		<ol>
@@ -276,7 +276,7 @@
 			<li>%i18n:desktop.tags.mk-2fa-setting.scan%<br><img src={ data.qr }></li>
 			<li>%i18n:desktop.tags.mk-2fa-setting.done%<br>
 				<input type="number" ref="token" class="ui">
-				<button onclick={ submit } class="ui primary">%i18n:desktop.tags.mk-2fa-setting.submit%</button>
+				<button @click="submit" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.submit%</button>
 			</li>
 		</ol>
 		<div class="ui info"><p>%fa:info-circle%%i18n:desktop.tags.mk-2fa-setting.info%</p></div>
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index ed77a9e60..0616a95f9 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -129,19 +129,19 @@
 			</div>
 			<footer>
 				<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
-				<button onclick={ reply } title="%i18n:desktop.tags.mk-timeline-post.reply%">
+				<button @click="reply" title="%i18n:desktop.tags.mk-timeline-post.reply%">
 					%fa:reply%<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
 				</button>
-				<button onclick={ repost } title="%i18n:desktop.tags.mk-timeline-post.repost%">
+				<button @click="repost" title="%i18n:desktop.tags.mk-timeline-post.repost%">
 					%fa:retweet%<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
 				</button>
-				<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="%i18n:desktop.tags.mk-timeline-post.add-reaction%">
+				<button class={ reacted: p.my_reaction != null } @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-post.add-reaction%">
 					%fa:plus%<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
 				</button>
-				<button onclick={ menu } ref="menuButton">
+				<button @click="menu" ref="menuButton">
 					%fa:ellipsis-h%
 				</button>
-				<button onclick={ toggleDetail } title="%i18n:desktop.tags.mk-timeline-post.detail">
+				<button @click="toggleDetail" title="%i18n:desktop.tags.mk-timeline-post.detail">
 					<virtual if={ !isDetailOpened }>%fa:caret-down%</virtual>
 					<virtual if={ isDetailOpened }>%fa:caret-up%</virtual>
 				</button>
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index 3dfdeec01..3e7b5c2ec 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -186,7 +186,7 @@
 </mk-ui-header-search>
 
 <mk-ui-header-post-button>
-	<button onclick={ post } title="%i18n:desktop.tags.mk-ui-header-post-button.post%">%fa:pencil-alt%</button>
+	<button @click="post" title="%i18n:desktop.tags.mk-ui-header-post-button.post%">%fa:pencil-alt%</button>
 	<style>
 		:scope
 			display inline-block
@@ -229,7 +229,7 @@
 </mk-ui-header-post-button>
 
 <mk-ui-header-notifications>
-	<button data-active={ isOpen } onclick={ toggle } title="%i18n:desktop.tags.mk-ui-header-notifications.title%">
+	<button data-active={ isOpen } @click="toggle" title="%i18n:desktop.tags.mk-ui-header-notifications.title%">
 		%fa:R bell%<virtual if={ hasUnreadNotifications }>%fa:circle%</virtual>
 	</button>
 	<div class="notifications" if={ isOpen }>
@@ -400,7 +400,7 @@
 				</a>
 			</li>
 			<li class="messaging">
-				<a onclick={ messaging }>
+				<a @click="messaging">
 					%fa:comments%
 					<p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p>
 					<virtual if={ hasUnreadMessagingMessages }>%fa:circle%</virtual>
@@ -629,7 +629,7 @@
 </mk-ui-header-clock>
 
 <mk-ui-header-account>
-	<button class="header" data-active={ isOpen.toString() } onclick={ toggle }>
+	<button class="header" data-active={ isOpen.toString() } @click="toggle">
 		<span class="username">{ I.username }<virtual if={ !isOpen }>%fa:angle-down%</virtual><virtual if={ isOpen }>%fa:angle-up%</virtual></span>
 		<img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 	</button>
@@ -638,7 +638,7 @@
 			<li>
 				<a href={ '/' + I.username }>%fa:user%%i18n:desktop.tags.mk-ui-header-account.profile%%fa:angle-right%</a>
 			</li>
-			<li onclick={ drive }>
+			<li @click="drive">
 				<p>%fa:cloud%%i18n:desktop.tags.mk-ui-header-account.drive%%fa:angle-right%</p>
 			</li>
 			<li>
@@ -646,12 +646,12 @@
 			</li>
 		</ul>
 		<ul>
-			<li onclick={ settings }>
+			<li @click="settings">
 				<p>%fa:cog%%i18n:desktop.tags.mk-ui-header-account.settings%%fa:angle-right%</p>
 			</li>
 		</ul>
 		<ul>
-			<li onclick={ signout }>
+			<li @click="signout">
 				<p>%fa:power-off%%i18n:desktop.tags.mk-ui-header-account.signout%%fa:angle-right%</p>
 			</li>
 		</ul>
diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/tags/user-timeline.tag
index 134aeee28..19ee2f328 100644
--- a/src/web/app/desktop/tags/user-timeline.tag
+++ b/src/web/app/desktop/tags/user-timeline.tag
@@ -1,6 +1,6 @@
 <mk-user-timeline>
 	<header>
-		<span data-is-active={ mode == 'default' } onclick={ setMode.bind(this, 'default') }>投稿</span><span data-is-active={ mode == 'with-replies' } onclick={ setMode.bind(this, 'with-replies') }>投稿と返信</span>
+		<span data-is-active={ mode == 'default' } @click="setMode.bind(this, 'default')">投稿</span><span data-is-active={ mode == 'with-replies' } @click="setMode.bind(this, 'with-replies')">投稿と返信</span>
 	</header>
 	<div class="loading" if={ isLoading }>
 		<mk-ellipsis-icon/>
diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/tags/user.tag
index b29d1eaeb..5dc4175cf 100644
--- a/src/web/app/desktop/tags/user.tag
+++ b/src/web/app/desktop/tags/user.tag
@@ -40,7 +40,7 @@
 
 <mk-user-header data-is-dark-background={ user.banner_url != null }>
 	<div class="banner-container" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=2048)' : '' }>
-		<div class="banner" ref="banner" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=2048)' : '' } onclick={ onUpdateBanner }></div>
+		<div class="banner" ref="banner" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=2048)' : '' } @click="onUpdateBanner"></div>
 	</div>
 	<div class="fade"></div>
 	<div class="container">
@@ -227,8 +227,8 @@
 	<div class="friend-form" if={ SIGNIN && I.id != user.id }>
 		<mk-big-follow-button user={ user }/>
 		<p class="followed" if={ user.is_followed }>%i18n:desktop.tags.mk-user.follows-you%</p>
-		<p if={ user.is_muted }>%i18n:desktop.tags.mk-user.muted% <a onclick={ unmute }>%i18n:desktop.tags.mk-user.unmute%</a></p>
-		<p if={ !user.is_muted }><a onclick={ mute }>%i18n:desktop.tags.mk-user.mute%</a></p>
+		<p if={ user.is_muted }>%i18n:desktop.tags.mk-user.muted% <a @click="unmute">%i18n:desktop.tags.mk-user.unmute%</a></p>
+		<p if={ !user.is_muted }><a @click="mute">%i18n:desktop.tags.mk-user.mute%</a></p>
 	</div>
 	<div class="description" if={ user.description }>{ user.description }</div>
 	<div class="birthday" if={ user.profile.birthday }>
@@ -239,8 +239,8 @@
 	</div>
 	<div class="status">
 	  <p class="posts-count">%fa:angle-right%<a>{ user.posts_count }</a><b>ポスト</b></p>
-		<p class="following">%fa:angle-right%<a onclick={ showFollowing }>{ user.following_count }</a>人を<b>フォロー</b></p>
-		<p class="followers">%fa:angle-right%<a onclick={ showFollowers }>{ user.followers_count }</a>人の<b>フォロワー</b></p>
+		<p class="following">%fa:angle-right%<a @click="showFollowing">{ user.following_count }</a>人を<b>フォロー</b></p>
+		<p class="followers">%fa:angle-right%<a @click="showFollowers">{ user.followers_count }</a>人の<b>フォロワー</b></p>
 	</div>
 	<style>
 		:scope
diff --git a/src/web/app/desktop/tags/users-list.tag b/src/web/app/desktop/tags/users-list.tag
index ec9c7d8c7..3e993a40e 100644
--- a/src/web/app/desktop/tags/users-list.tag
+++ b/src/web/app/desktop/tags/users-list.tag
@@ -1,8 +1,8 @@
 <mk-users-list>
 	<nav>
 		<div>
-			<span data-is-active={ mode == 'all' } onclick={ setMode.bind(this, 'all') }>すべて<span>{ opts.count }</span></span>
-			<span if={ SIGNIN && opts.youKnowCount } data-is-active={ mode == 'iknow' } onclick={ setMode.bind(this, 'iknow') }>知り合い<span>{ opts.youKnowCount }</span></span>
+			<span data-is-active={ mode == 'all' } @click="setMode.bind(this, 'all')">すべて<span>{ opts.count }</span></span>
+			<span if={ SIGNIN && opts.youKnowCount } data-is-active={ mode == 'iknow' } @click="setMode.bind(this, 'iknow')">知り合い<span>{ opts.youKnowCount }</span></span>
 		</div>
 	</nav>
 	<div class="users" if={ !fetching && users.length != 0 }>
@@ -10,7 +10,7 @@
 			<mk-list-user user={ this }/>
 		</div>
 	</div>
-	<button class="more" if={ !fetching && next != null } onclick={ more } disabled={ moreFetching }>
+	<button class="more" if={ !fetching && next != null } @click="more" disabled={ moreFetching }>
 		<span if={ !moreFetching }>もっと</span>
 		<span if={ moreFetching }>読み込み中<mk-ellipsis/></span>
 	</button>
diff --git a/src/web/app/desktop/tags/widgets/activity.tag b/src/web/app/desktop/tags/widgets/activity.tag
index e8c8a4763..9b547b95f 100644
--- a/src/web/app/desktop/tags/widgets/activity.tag
+++ b/src/web/app/desktop/tags/widgets/activity.tag
@@ -1,7 +1,7 @@
 <mk-activity-widget data-melt={ design == 2 }>
 	<virtual if={ design == 0 }>
 		<p class="title">%fa:chart-bar%%i18n:desktop.tags.mk-activity-widget.title%</p>
-		<button onclick={ toggle } title="%i18n:desktop.tags.mk-activity-widget.toggle%">%fa:sort%</button>
+		<button @click="toggle" title="%i18n:desktop.tags.mk-activity-widget.toggle%">%fa:sort%</button>
 	</virtual>
 	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<mk-activity-widget-calender if={ !initializing && view == 0 } data={ [].concat(activity) }/>
diff --git a/src/web/app/desktop/tags/widgets/calendar.tag b/src/web/app/desktop/tags/widgets/calendar.tag
index abe998187..00205a90a 100644
--- a/src/web/app/desktop/tags/widgets/calendar.tag
+++ b/src/web/app/desktop/tags/widgets/calendar.tag
@@ -1,8 +1,8 @@
 <mk-calendar-widget data-melt={ opts.design == 4 || opts.design == 5 }>
 	<virtual if={ opts.design == 0 || opts.design == 1 }>
-		<button onclick={ prev } title="%i18n:desktop.tags.mk-calendar-widget.prev%">%fa:chevron-circle-left%</button>
+		<button @click="prev" title="%i18n:desktop.tags.mk-calendar-widget.prev%">%fa:chevron-circle-left%</button>
 		<p class="title">{ '%i18n:desktop.tags.mk-calendar-widget.title%'.replace('{1}', year).replace('{2}', month) }</p>
-		<button onclick={ next } title="%i18n:desktop.tags.mk-calendar-widget.next%">%fa:chevron-circle-right%</button>
+		<button @click="next" title="%i18n:desktop.tags.mk-calendar-widget.next%">%fa:chevron-circle-right%</button>
 	</virtual>
 
 	<div class="calendar">
@@ -15,7 +15,7 @@
 				data-selected={ isSelected(i + 1) }
 				data-is-out-of-range={ isOutOfRange(i + 1) }
 				data-is-donichi={ isDonichi(i + 1) }
-				onclick={ go.bind(null, i + 1) }
+				@click="go.bind(null, i + 1)"
 				title={ isOutOfRange(i + 1) ? null : '%i18n:desktop.tags.mk-calendar-widget.go%' }><div>{ i + 1 }</div></div>
 	</div>
 	<style>
diff --git a/src/web/app/desktop/tags/window.tag b/src/web/app/desktop/tags/window.tag
index 5b4b3c83e..ebc7382d5 100644
--- a/src/web/app/desktop/tags/window.tag
+++ b/src/web/app/desktop/tags/window.tag
@@ -1,12 +1,12 @@
 <mk-window data-flexible={ isFlexible } ondragover={ ondragover }>
-	<div class="bg" ref="bg" show={ isModal } onclick={ bgClick }></div>
+	<div class="bg" ref="bg" show={ isModal } @click="bgClick"></div>
 	<div class="main" ref="main" tabindex="-1" data-is-modal={ isModal } onmousedown={ onBodyMousedown } onkeydown={ onKeydown }>
 		<div class="body">
 			<header ref="header" onmousedown={ onHeaderMousedown }>
 				<h1 data-yield="header"><yield from="header"/></h1>
 				<div>
-					<button class="popout" if={ popoutUrl } onmousedown={ repelMove } onclick={ popout } title="ポップアウト">%fa:R window-restore%</button>
-					<button class="close" if={ canClose } onmousedown={ repelMove } onclick={ close } title="閉じる">%fa:times%</button>
+					<button class="popout" if={ popoutUrl } onmousedown={ repelMove } @click="popout" title="ポップアウト">%fa:R window-restore%</button>
+					<button class="close" if={ canClose } onmousedown={ repelMove } @click="close" title="閉じる">%fa:times%</button>
 				</div>
 			</header>
 			<div class="content" data-yield="content"><yield from="content"/></div>
diff --git a/src/web/app/dev/tags/new-app-form.tag b/src/web/app/dev/tags/new-app-form.tag
index fdd442a83..c9518d8de 100644
--- a/src/web/app/dev/tags/new-app-form.tag
+++ b/src/web/app/dev/tags/new-app-form.tag
@@ -73,7 +73,7 @@
 			</div>
 			<p>%fa:exclamation-triangle%アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。</p>
 		</section>
-		<button onclick={ onsubmit }>アプリ作成</button>
+		<button @click="onsubmit">アプリ作成</button>
 	</form>
 	<style>
 		:scope
diff --git a/src/web/app/mobile/tags/drive-folder-selector.tag b/src/web/app/mobile/tags/drive-folder-selector.tag
index 35d0208a0..82e22fed2 100644
--- a/src/web/app/mobile/tags/drive-folder-selector.tag
+++ b/src/web/app/mobile/tags/drive-folder-selector.tag
@@ -2,8 +2,8 @@
 	<div class="body">
 		<header>
 			<h1>%i18n:mobile.tags.mk-drive-folder-selector.select-folder%</h1>
-			<button class="close" onclick={ cancel }>%fa:times%</button>
-			<button class="ok" onclick={ ok }>%fa:check%</button>
+			<button class="close" @click="cancel">%fa:times%</button>
+			<button class="ok" @click="ok">%fa:check%</button>
 		</header>
 		<mk-drive ref="browser" select-folder={ true }/>
 	</div>
diff --git a/src/web/app/mobile/tags/drive-selector.tag b/src/web/app/mobile/tags/drive-selector.tag
index f8bc49dab..36fed8c32 100644
--- a/src/web/app/mobile/tags/drive-selector.tag
+++ b/src/web/app/mobile/tags/drive-selector.tag
@@ -2,8 +2,8 @@
 	<div class="body">
 		<header>
 			<h1>%i18n:mobile.tags.mk-drive-selector.select-file%<span class="count" if={ files.length > 0 }>({ files.length })</span></h1>
-			<button class="close" onclick={ cancel }>%fa:times%</button>
-			<button if={ opts.multiple } class="ok" onclick={ ok }>%fa:check%</button>
+			<button class="close" @click="cancel">%fa:times%</button>
+			<button if={ opts.multiple } class="ok" @click="ok">%fa:check%</button>
 		</header>
 		<mk-drive ref="browser" select-file={ true } multiple={ opts.multiple }/>
 	</div>
diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index 2a3ff23bf..d3ca1aff9 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -1,9 +1,9 @@
 <mk-drive>
 	<nav ref="nav">
-		<a onclick={ goRoot } href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</a>
+		<a @click="goRoot" href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</a>
 		<virtual each={ folder in hierarchyFolders }>
 			<span>%fa:angle-right%</span>
-			<a onclick={ move } href="/i/drive/folder/{ folder.id }">{ folder.name }</a>
+			<a @click="move" href="/i/drive/folder/{ folder.id }">{ folder.name }</a>
 		</virtual>
 		<virtual if={ folder != null }>
 			<span>%fa:angle-right%</span>
@@ -34,7 +34,7 @@
 			<virtual each={ file in files }>
 				<mk-drive-file file={ file }/>
 			</virtual>
-			<button class="more" if={ moreFiles } onclick={ fetchMoreFiles }>
+			<button class="more" if={ moreFiles } @click="fetchMoreFiles">
 				{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-drive.load-more%' }
 			</button>
 		</div>
diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag
index 259873d95..2d9338fd3 100644
--- a/src/web/app/mobile/tags/drive/file-viewer.tag
+++ b/src/web/app/mobile/tags/drive/file-viewer.tag
@@ -28,7 +28,7 @@
 			<span class="separator"></span>
 			<span class="data-size">{ bytesToSize(file.datasize) }</span>
 			<span class="separator"></span>
-			<span class="created-at" onclick={ showCreatedAt }>%fa:R clock%<mk-time time={ file.created_at }/></span>
+			<span class="created-at" @click="showCreatedAt">%fa:R clock%<mk-time time={ file.created_at }/></span>
 		</div>
 	</div>
 	<div class="menu">
@@ -36,10 +36,10 @@
 			<a href={ file.url + '?download' } download={ file.name }>
 				%fa:download%%i18n:mobile.tags.mk-drive-file-viewer.download%
 			</a>
-			<button onclick={ rename }>
+			<button @click="rename">
 				%fa:pencil-alt%%i18n:mobile.tags.mk-drive-file-viewer.rename%
 			</button>
-			<button onclick={ move }>
+			<button @click="move">
 				%fa:R folder-open%%i18n:mobile.tags.mk-drive-file-viewer.move%
 			</button>
 		</div>
diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag
index 684df7dd0..a04528ce7 100644
--- a/src/web/app/mobile/tags/drive/file.tag
+++ b/src/web/app/mobile/tags/drive/file.tag
@@ -1,5 +1,5 @@
 <mk-drive-file data-is-selected={ isSelected }>
-	<a onclick={ onclick } href="/i/drive/file/{ file.id }">
+	<a @click="onclick" href="/i/drive/file/{ file.id }">
 		<div class="container">
 			<div class="thumbnail" style={ thumbnail }></div>
 			<div class="body">
diff --git a/src/web/app/mobile/tags/drive/folder.tag b/src/web/app/mobile/tags/drive/folder.tag
index 6125e0b25..c0ccee6a5 100644
--- a/src/web/app/mobile/tags/drive/folder.tag
+++ b/src/web/app/mobile/tags/drive/folder.tag
@@ -1,5 +1,5 @@
 <mk-drive-folder>
-	<a onclick={ onclick } href="/i/drive/folder/{ folder.id }">
+	<a @click="onclick" href="/i/drive/folder/{ folder.id }">
 		<div class="container">
 			<p class="name">%fa:folder%{ folder.name }</p>%fa:angle-right%
 		</div>
diff --git a/src/web/app/mobile/tags/follow-button.tag b/src/web/app/mobile/tags/follow-button.tag
index 5b710bfa9..805d5e659 100644
--- a/src/web/app/mobile/tags/follow-button.tag
+++ b/src/web/app/mobile/tags/follow-button.tag
@@ -1,5 +1,5 @@
 <mk-follow-button>
-	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } if={ !init } onclick={ onclick } disabled={ wait }>
+	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } if={ !init } @click="onclick" disabled={ wait }>
 		<virtual if={ !wait && user.is_following }>%fa:minus%</virtual>
 		<virtual if={ !wait && !user.is_following }>%fa:plus%</virtual>
 		<virtual if={ wait }>%fa:spinner .pulse .fw%</virtual>{ user.is_following ? '%i18n:mobile.tags.mk-follow-button.unfollow%' : '%i18n:mobile.tags.mk-follow-button.follow%' }
diff --git a/src/web/app/mobile/tags/init-following.tag b/src/web/app/mobile/tags/init-following.tag
index 105a1f70d..d2d19a887 100644
--- a/src/web/app/mobile/tags/init-following.tag
+++ b/src/web/app/mobile/tags/init-following.tag
@@ -7,8 +7,8 @@
 	</div>
 	<p class="empty" if={ !fetching && users.length == 0 }>おすすめのユーザーは見つかりませんでした。</p>
 	<p class="fetching" if={ fetching }>%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
-	<a class="refresh" onclick={ refresh }>もっと見る</a>
-	<button class="close" onclick={ close } title="閉じる">%fa:times%</button>
+	<a class="refresh" @click="refresh">もっと見る</a>
+	<button class="close" @click="close" title="閉じる">%fa:times%</button>
 	<style>
 		:scope
 			display block
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index 742cc4514..520a336b0 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -5,7 +5,7 @@
 			<p class="date" if={ i != notifications.length - 1 && notification._date != notifications[i + 1]._date }><span>%fa:angle-up%{ notification._datetext }</span><span>%fa:angle-down%{ notifications[i + 1]._datetext }</span></p>
 		</virtual>
 	</div>
-	<button class="more" if={ moreNotifications } onclick={ fetchMoreNotifications } disabled={ fetchingMoreNotifications }>
+	<button class="more" if={ moreNotifications } @click="fetchMoreNotifications" disabled={ fetchingMoreNotifications }>
 		<virtual if={ fetchingMoreNotifications }>%fa:spinner .pulse .fw%</virtual>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-notifications.more%' }
 	</button>
 	<p class="empty" if={ notifications.length == 0 && !loading }>%i18n:mobile.tags.mk-notifications.empty%</p>
diff --git a/src/web/app/mobile/tags/page/entrance.tag b/src/web/app/mobile/tags/page/entrance.tag
index 191874caf..b5da3c947 100644
--- a/src/web/app/mobile/tags/page/entrance.tag
+++ b/src/web/app/mobile/tags/page/entrance.tag
@@ -4,7 +4,7 @@
 		<mk-entrance-signup if={ mode == 'signup' }/>
 		<div class="introduction" if={ mode == 'introduction' }>
 			<mk-introduction/>
-			<button onclick={ signin }>%i18n:common.ok%</button>
+			<button @click="signin">%i18n:common.ok%</button>
 		</div>
 	</main>
 	<footer>
diff --git a/src/web/app/mobile/tags/page/entrance/signin.tag b/src/web/app/mobile/tags/page/entrance/signin.tag
index 6f473feb9..81d0a48a7 100644
--- a/src/web/app/mobile/tags/page/entrance/signin.tag
+++ b/src/web/app/mobile/tags/page/entrance/signin.tag
@@ -2,7 +2,7 @@
 	<mk-signin/>
 	<a href={ _API_URL_ + '/signin/twitter' }>Twitterでサインイン</a>
 	<div class="divider"><span>or</span></div>
-	<button class="signup" onclick={ parent.signup }>%i18n:mobile.tags.mk-entrance-signin.signup%</button><a class="introduction" onclick={ parent.introduction }>%i18n:mobile.tags.mk-entrance-signin.about%</a>
+	<button class="signup" @click="parent.signup">%i18n:mobile.tags.mk-entrance-signin.signup%</button><a class="introduction" @click="parent.introduction">%i18n:mobile.tags.mk-entrance-signin.about%</a>
 	<style>
 		:scope
 			display block
diff --git a/src/web/app/mobile/tags/page/entrance/signup.tag b/src/web/app/mobile/tags/page/entrance/signup.tag
index 7b11bcad4..6634593d3 100644
--- a/src/web/app/mobile/tags/page/entrance/signup.tag
+++ b/src/web/app/mobile/tags/page/entrance/signup.tag
@@ -1,6 +1,6 @@
 <mk-entrance-signup>
 	<mk-signup/>
-	<button class="cancel" type="button" onclick={ parent.signin } title="%i18n:mobile.tags.mk-entrance-signup.cancel%">%fa:times%</button>
+	<button class="cancel" type="button" @click="parent.signin" title="%i18n:mobile.tags.mk-entrance-signup.cancel%">%fa:times%</button>
 	<style>
 		:scope
 			display block
diff --git a/src/web/app/mobile/tags/page/selectdrive.tag b/src/web/app/mobile/tags/page/selectdrive.tag
index 1a790d806..42a624a7a 100644
--- a/src/web/app/mobile/tags/page/selectdrive.tag
+++ b/src/web/app/mobile/tags/page/selectdrive.tag
@@ -1,8 +1,8 @@
 <mk-selectdrive-page>
 	<header>
 		<h1>%i18n:mobile.tags.mk-selectdrive-page.select-file%<span class="count" if={ files.length > 0 }>({ files.length })</span></h1>
-		<button class="upload" onclick={ upload }>%fa:upload%</button>
-		<button if={ multiple } class="ok" onclick={ ok }>%fa:check%</button>
+		<button class="upload" @click="upload">%fa:upload%</button>
+		<button if={ multiple } class="ok" @click="ok">%fa:check%</button>
 	</header>
 	<mk-drive ref="browser" select-file={ true } multiple={ multiple } is-naked={ true } top={ 42 }/>
 
diff --git a/src/web/app/mobile/tags/page/settings.tag b/src/web/app/mobile/tags/page/settings.tag
index 9a73b0af3..b388121cb 100644
--- a/src/web/app/mobile/tags/page/settings.tag
+++ b/src/web/app/mobile/tags/page/settings.tag
@@ -26,7 +26,7 @@
 		<li><a href="./settings/signin-history">%fa:sign-in-alt%%i18n:mobile.tags.mk-settings-page.signin-history%%fa:angle-right%</a></li>
 	</ul>
 	<ul>
-		<li><a onclick={ signout }>%fa:power-off%%i18n:mobile.tags.mk-settings-page.signout%</a></li>
+		<li><a @click="signout">%fa:power-off%%i18n:mobile.tags.mk-settings-page.signout%</a></li>
 	</ul>
 	<p><small>ver { _VERSION_ } (葵 aoi)</small></p>
 	<style>
diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
index 8881e9519..cf62c3eb5 100644
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -21,8 +21,8 @@
 	<div>
 		<p>%fa:info-circle%%i18n:mobile.tags.mk-profile-setting.will-be-published%</p>
 		<div class="form">
-			<div style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=1024)' : '' } onclick={ clickBanner }>
-				<img src={ I.avatar_url + '?thumbnail&size=200' } alt="avatar" onclick={ clickAvatar }/>
+			<div style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=1024)' : '' } @click="clickBanner">
+				<img src={ I.avatar_url + '?thumbnail&size=200' } alt="avatar" @click="clickAvatar"/>
 			</div>
 			<label>
 				<p>%i18n:mobile.tags.mk-profile-setting.name%</p>
@@ -42,14 +42,14 @@
 			</label>
 			<label>
 				<p>%i18n:mobile.tags.mk-profile-setting.avatar%</p>
-				<button onclick={ setAvatar } disabled={ avatarSaving }>%i18n:mobile.tags.mk-profile-setting.set-avatar%</button>
+				<button @click="setAvatar" disabled={ avatarSaving }>%i18n:mobile.tags.mk-profile-setting.set-avatar%</button>
 			</label>
 			<label>
 				<p>%i18n:mobile.tags.mk-profile-setting.banner%</p>
-				<button onclick={ setBanner } disabled={ bannerSaving }>%i18n:mobile.tags.mk-profile-setting.set-banner%</button>
+				<button @click="setBanner" disabled={ bannerSaving }>%i18n:mobile.tags.mk-profile-setting.set-banner%</button>
 			</label>
 		</div>
-		<button class="save" onclick={ save } disabled={ saving }>%fa:check%%i18n:mobile.tags.mk-profile-setting.save%</button>
+		<button class="save" @click="save" disabled={ saving }>%fa:check%%i18n:mobile.tags.mk-profile-setting.save%</button>
 	</div>
 	<style>
 		:scope
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index 1816d1bf9..131ea3aa3 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -1,5 +1,5 @@
 <mk-post-detail>
-	<button class="read-more" if={ p.reply && p.reply.reply_id && context == null } onclick={ loadContext } disabled={ loadingContext }>
+	<button class="read-more" if={ p.reply && p.reply.reply_id && context == null } @click="loadContext" disabled={ loadingContext }>
 		<virtual if={ !contextFetching }>%fa:ellipsis-v%</virtual>
 		<virtual if={ contextFetching }>%fa:spinner .pulse%</virtual>
 	</button>
@@ -43,16 +43,16 @@
 		</a>
 		<footer>
 			<mk-reactions-viewer post={ p }/>
-			<button onclick={ reply } title="%i18n:mobile.tags.mk-post-detail.reply%">
+			<button @click="reply" title="%i18n:mobile.tags.mk-post-detail.reply%">
 				%fa:reply%<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
 			</button>
-			<button onclick={ repost } title="Repost">
+			<button @click="repost" title="Repost">
 				%fa:retweet%<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
 			</button>
-			<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton" title="%i18n:mobile.tags.mk-post-detail.reaction%">
+			<button class={ reacted: p.my_reaction != null } @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-post-detail.reaction%">
 				%fa:plus%<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
 			</button>
-			<button onclick={ menu } ref="menuButton">
+			<button @click="menu" ref="menuButton">
 				%fa:ellipsis-h%
 			</button>
 		</footer>
diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
index 05466a6ec..f0aa102d6 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -1,9 +1,9 @@
 <mk-post-form>
 	<header>
-		<button class="cancel" onclick={ cancel }>%fa:times%</button>
+		<button class="cancel" @click="cancel">%fa:times%</button>
 		<div>
 			<span if={ refs.text } class="text-count { over: refs.text.value.length > 1000 }">{ 1000 - refs.text.value.length }</span>
-			<button class="submit" onclick={ post }>%i18n:mobile.tags.mk-post-form.submit%</button>
+			<button class="submit" @click="post">%i18n:mobile.tags.mk-post-form.submit%</button>
 		</div>
 	</header>
 	<div class="form">
@@ -12,16 +12,16 @@
 		<div class="attaches" show={ files.length != 0 }>
 			<ul class="files" ref="attaches">
 				<li class="file" each={ files } data-id={ id }>
-					<div class="img" style="background-image: url({ url + '?thumbnail&size=128' })" onclick={ removeFile }></div>
+					<div class="img" style="background-image: url({ url + '?thumbnail&size=128' })" @click="removeFile"></div>
 				</li>
 			</ul>
 		</div>
 		<mk-poll-editor if={ poll } ref="poll" ondestroy={ onPollDestroyed }/>
 		<mk-uploader ref="uploader"/>
-		<button ref="upload" onclick={ selectFile }>%fa:upload%</button>
-		<button ref="drive" onclick={ selectFileFromDrive }>%fa:cloud%</button>
-		<button class="kao" onclick={ kao }>%fa:R smile%</button>
-		<button class="poll" onclick={ addPoll }>%fa:chart-pie%</button>
+		<button ref="upload" @click="selectFile">%fa:upload%</button>
+		<button ref="drive" @click="selectFileFromDrive">%fa:cloud%</button>
+		<button class="kao" @click="kao">%fa:R smile%</button>
+		<button class="poll" @click="addPoll">%fa:chart-pie%</button>
 		<input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
 	</div>
 	<style>
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 9e85f97da..400fa5d85 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -13,7 +13,7 @@
 		</p>
 	</virtual>
 	<footer if={ !init }>
-		<button if={ canFetchMore } onclick={ more } disabled={ fetching }>
+		<button if={ canFetchMore } @click="more" disabled={ fetching }>
 			<span if={ !fetching }>%i18n:mobile.tags.mk-timeline.load-more%</span>
 			<span if={ fetching }>%i18n:common.loading%<mk-ellipsis/></span>
 		</button>
@@ -182,16 +182,16 @@
 			</div>
 			<footer>
 				<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
-				<button onclick={ reply }>
+				<button @click="reply">
 					%fa:reply%<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
 				</button>
-				<button onclick={ repost } title="Repost">
+				<button @click="repost" title="Repost">
 					%fa:retweet%<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
 				</button>
-				<button class={ reacted: p.my_reaction != null } onclick={ react } ref="reactButton">
+				<button class={ reacted: p.my_reaction != null } @click="react" ref="reactButton">
 					%fa:plus%<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
 				</button>
-				<button class="menu" onclick={ menu } ref="menuButton">
+				<button class="menu" @click="menu" ref="menuButton">
 					%fa:ellipsis-h%
 				</button>
 			</footer>
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index 77ad14530..b03534f92 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -52,10 +52,10 @@
 	<div class="main">
 		<div class="backdrop"></div>
 		<div class="content">
-			<button class="nav" onclick={ parent.toggleDrawer }>%fa:bars%</button>
+			<button class="nav" @click="parent.toggleDrawer">%fa:bars%</button>
 			<virtual if={ hasUnreadNotifications || hasUnreadMessagingMessages }>%fa:circle%</virtual>
 			<h1 ref="title">Misskey</h1>
-			<button if={ func } onclick={ func }><mk-raw content={ funcIcon }/></button>
+			<button if={ func } @click="func"><mk-raw content={ funcIcon }/></button>
 		</div>
 	</div>
 	<style>
@@ -225,7 +225,7 @@
 </mk-ui-header>
 
 <mk-ui-nav>
-	<div class="backdrop" onclick={ parent.toggleDrawer }></div>
+	<div class="backdrop" @click="parent.toggleDrawer"></div>
 	<div class="body">
 		<a class="me" if={ SIGNIN } href={ '/' + I.username }>
 			<img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/>
@@ -242,7 +242,7 @@
 				<li><a href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-ui-nav.drive%%fa:angle-right%</a></li>
 			</ul>
 			<ul>
-				<li><a onclick={ search }>%fa:search%%i18n:mobile.tags.mk-ui-nav.search%%fa:angle-right%</a></li>
+				<li><a @click="search">%fa:search%%i18n:mobile.tags.mk-ui-nav.search%%fa:angle-right%</a></li>
 			</ul>
 			<ul>
 				<li><a href="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</a></li>
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index b3a2f1a14..eb6d9ffbe 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -39,9 +39,9 @@
 				</div>
 			</div>
 			<nav>
-				<a data-is-active={ page == 'overview' } onclick={ go.bind(null, 'overview') }>%i18n:mobile.tags.mk-user.overview%</a>
-				<a data-is-active={ page == 'posts' } onclick={ go.bind(null, 'posts') }>%i18n:mobile.tags.mk-user.timeline%</a>
-				<a data-is-active={ page == 'media' } onclick={ go.bind(null, 'media') }>%i18n:mobile.tags.mk-user.media%</a>
+				<a data-is-active={ page == 'overview' } @click="go.bind(null, 'overview')">%i18n:mobile.tags.mk-user.overview%</a>
+				<a data-is-active={ page == 'posts' } @click="go.bind(null, 'posts')">%i18n:mobile.tags.mk-user.timeline%</a>
+				<a data-is-active={ page == 'media' } @click="go.bind(null, 'media')">%i18n:mobile.tags.mk-user.media%</a>
 			</nav>
 		</header>
 		<div class="body">
diff --git a/src/web/app/mobile/tags/users-list.tag b/src/web/app/mobile/tags/users-list.tag
index 1dec33ddd..8e18bdea3 100644
--- a/src/web/app/mobile/tags/users-list.tag
+++ b/src/web/app/mobile/tags/users-list.tag
@@ -1,12 +1,12 @@
 <mk-users-list>
 	<nav>
-		<span data-is-active={ mode == 'all' } onclick={ setMode.bind(this, 'all') }>%i18n:mobile.tags.mk-users-list.all%<span>{ opts.count }</span></span>
-		<span if={ SIGNIN && opts.youKnowCount } data-is-active={ mode == 'iknow' } onclick={ setMode.bind(this, 'iknow') }>%i18n:mobile.tags.mk-users-list.known%<span>{ opts.youKnowCount }</span></span>
+		<span data-is-active={ mode == 'all' } @click="setMode.bind(this, 'all')">%i18n:mobile.tags.mk-users-list.all%<span>{ opts.count }</span></span>
+		<span if={ SIGNIN && opts.youKnowCount } data-is-active={ mode == 'iknow' } @click="setMode.bind(this, 'iknow')">%i18n:mobile.tags.mk-users-list.known%<span>{ opts.youKnowCount }</span></span>
 	</nav>
 	<div class="users" if={ !fetching && users.length != 0 }>
 		<mk-user-preview each={ users } user={ this }/>
 	</div>
-	<button class="more" if={ !fetching && next != null } onclick={ more } disabled={ moreFetching }>
+	<button class="more" if={ !fetching && next != null } @click="more" disabled={ moreFetching }>
 		<span if={ !moreFetching }>%i18n:mobile.tags.mk-users-list.load-more%</span>
 		<span if={ moreFetching }>%i18n:common.loading%<mk-ellipsis/></span></button>
 	<p class="no" if={ !fetching && users.length == 0 }>{ opts.noUsers }</p>

From ea70350dcc41a142653f09341b100d7f5712ed99 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 7 Feb 2018 15:34:08 +0900
Subject: [PATCH 004/286] wip

---
 src/web/app/common/tags/reaction-picker.vue | 31 +++++++++------------
 1 file changed, 13 insertions(+), 18 deletions(-)

diff --git a/src/web/app/common/tags/reaction-picker.vue b/src/web/app/common/tags/reaction-picker.vue
index 243039030..970b7036d 100644
--- a/src/web/app/common/tags/reaction-picker.vue
+++ b/src/web/app/common/tags/reaction-picker.vue
@@ -2,7 +2,7 @@
 <div>
 	<div class="backdrop" ref="backdrop" @click="close"></div>
 	<div class="popover" :data-compact="compact" ref="popover">
-		<p if={ !opts.compact }>{ title }</p>
+		<p v-if="!compact">{{ title }}</p>
 		<div>
 			<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>
 			<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button>
@@ -22,10 +22,15 @@
 	import anime from 'animejs';
 	import api from '../scripts/api';
 
+	const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%';
+
 	export default {
 		props: ['post', 'cb'],
+		data: {
+			title: placeholder
+		},
 		methods: {
-			react: function (reaction) {
+			react: function(reaction) {
 				api('posts/reactions/create', {
 					post_id: this.post.id,
 					reaction: reaction
@@ -33,6 +38,12 @@
 					if (this.cb) this.cb();
 					this.$destroy();
 				});
+			},
+			onMouseover: function(e) {
+				this.title = e.target.title;
+			},
+			onMouseout: function(e) {
+				this.title = placeholder;
 			}
 		}
 	};
@@ -42,22 +53,6 @@
 	this.post = this.opts.post;
 	this.source = this.opts.source;
 
-	const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%';
-
-	this.title = placeholder;
-
-	this.onmouseover = e => {
-		this.update({
-			title: e.target.title
-		});
-	};
-
-	this.onmouseout = () => {
-		this.update({
-			title: placeholder
-		});
-	};
-
 	this.on('mount', () => {
 		const rect = this.source.getBoundingClientRect();
 		const width = this.refs.popover.offsetWidth;

From 07efc8e1504450b4eb6bc29f43986a9d538d0680 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 7 Feb 2018 18:17:59 +0900
Subject: [PATCH 005/286] wip

---
 src/web/app/auth/tags/index.tag               |  4 +-
 src/web/app/ch/tags/channel.tag               | 12 +--
 src/web/app/common/tags/messaging/form.tag    |  6 +-
 src/web/app/common/tags/messaging/index.tag   | 10 +--
 src/web/app/common/tags/messaging/message.tag |  6 +-
 src/web/app/common/tags/messaging/room.tag    |  2 +-
 src/web/app/common/tags/poll-editor.tag       |  2 +-
 src/web/app/common/tags/post-menu.tag         | 26 +++---
 src/web/app/common/tags/reaction-picker.vue   | 68 +++++++-------
 src/web/app/common/tags/signin-history.tag    |  2 +-
 src/web/app/common/tags/signin.tag            | 20 ++---
 src/web/app/common/tags/signup.tag            | 14 +--
 src/web/app/desktop/tags/analog-clock.tag     |  6 +-
 .../desktop/tags/autocomplete-suggestion.tag  |  6 +-
 src/web/app/desktop/tags/crop-window.tag      |  8 +-
 .../app/desktop/tags/detailed-post-window.tag |  4 +-
 src/web/app/desktop/tags/dialog.tag           | 18 ++--
 .../desktop/tags/drive/base-contextmenu.tag   | 10 +--
 .../app/desktop/tags/drive/browser-window.tag |  6 +-
 src/web/app/desktop/tags/drive/browser.tag    | 42 ++++-----
 .../desktop/tags/drive/file-contextmenu.tag   | 14 +--
 src/web/app/desktop/tags/drive/file.tag       |  2 +-
 .../desktop/tags/drive/folder-contextmenu.tag | 12 +--
 .../desktop/tags/home-widgets/access-log.tag  |  2 +-
 .../desktop/tags/home-widgets/activity.tag    |  4 +-
 .../app/desktop/tags/home-widgets/channel.tag |  8 +-
 .../desktop/tags/home-widgets/mentions.tag    | 10 +--
 .../desktop/tags/home-widgets/messaging.tag   |  2 +-
 .../desktop/tags/home-widgets/post-form.tag   |  4 +-
 .../app/desktop/tags/home-widgets/server.tag  |  6 +-
 .../desktop/tags/home-widgets/slideshow.tag   | 12 +--
 .../desktop/tags/home-widgets/timeline.tag    | 12 +--
 .../app/desktop/tags/home-widgets/tips.tag    |  6 +-
 src/web/app/desktop/tags/home.tag             | 44 +++++-----
 src/web/app/desktop/tags/images.tag           | 12 +--
 src/web/app/desktop/tags/input-dialog.tag     | 10 +--
 .../desktop/tags/messaging/room-window.tag    |  2 +-
 src/web/app/desktop/tags/messaging/window.tag |  4 +-
 src/web/app/desktop/tags/pages/drive.tag      |  4 +-
 src/web/app/desktop/tags/pages/entrance.tag   |  2 +-
 src/web/app/desktop/tags/pages/home.tag       |  2 +-
 src/web/app/desktop/tags/pages/search.tag     |  2 +-
 .../app/desktop/tags/pages/selectdrive.tag    |  6 +-
 src/web/app/desktop/tags/pages/user.tag       |  4 +-
 src/web/app/desktop/tags/post-detail-sub.tag  |  4 +-
 src/web/app/desktop/tags/post-detail.tag      | 10 +--
 src/web/app/desktop/tags/post-form-window.tag | 12 +--
 src/web/app/desktop/tags/post-form.tag        | 34 +++----
 src/web/app/desktop/tags/progress-dialog.tag  |  4 +-
 .../app/desktop/tags/repost-form-window.tag   | 12 +--
 src/web/app/desktop/tags/repost-form.tag      |  4 +-
 src/web/app/desktop/tags/search-posts.tag     |  6 +-
 src/web/app/desktop/tags/search.tag           |  2 +-
 .../tags/select-file-from-drive-window.tag    | 12 +--
 .../tags/select-folder-from-drive-window.tag  |  8 +-
 src/web/app/desktop/tags/settings-window.tag  |  4 +-
 src/web/app/desktop/tags/settings.tag         | 10 +--
 src/web/app/desktop/tags/sub-post-content.tag |  4 +-
 src/web/app/desktop/tags/timeline.tag         | 14 +--
 src/web/app/desktop/tags/ui.tag               |  2 +-
 src/web/app/desktop/tags/user-timeline.tag    | 10 +--
 src/web/app/desktop/tags/user.tag             | 12 +--
 src/web/app/desktop/tags/window.tag           | 88 +++++++++----------
 src/web/app/dev/tags/new-app-form.tag         | 12 +--
 .../app/mobile/tags/drive-folder-selector.tag |  2 +-
 src/web/app/mobile/tags/drive-selector.tag    |  4 +-
 src/web/app/mobile/tags/drive.tag             |  6 +-
 src/web/app/mobile/tags/drive/file-viewer.tag |  2 +-
 src/web/app/mobile/tags/home-timeline.tag     |  6 +-
 src/web/app/mobile/tags/home.tag              |  2 +-
 src/web/app/mobile/tags/page/drive.tag        | 14 +--
 src/web/app/mobile/tags/page/home.tag         |  2 +-
 src/web/app/mobile/tags/page/messaging.tag    |  2 +-
 .../app/mobile/tags/page/notifications.tag    |  2 +-
 src/web/app/mobile/tags/page/search.tag       |  2 +-
 src/web/app/mobile/tags/page/selectdrive.tag  |  6 +-
 .../app/mobile/tags/page/settings/profile.tag |  8 +-
 .../app/mobile/tags/page/user-followers.tag   |  2 +-
 .../app/mobile/tags/page/user-following.tag   |  2 +-
 src/web/app/mobile/tags/page/user.tag         |  2 +-
 src/web/app/mobile/tags/post-detail.tag       | 10 +--
 src/web/app/mobile/tags/post-form.tag         | 22 ++---
 src/web/app/mobile/tags/search.tag            |  2 +-
 src/web/app/mobile/tags/sub-post-content.tag  |  4 +-
 src/web/app/mobile/tags/timeline.tag          | 14 +--
 src/web/app/mobile/tags/ui.tag                |  4 +-
 src/web/app/mobile/tags/user-followers.tag    |  2 +-
 src/web/app/mobile/tags/user-following.tag    |  2 +-
 src/web/app/mobile/tags/user-timeline.tag     |  2 +-
 src/web/app/status/tags/index.tag             |  4 +-
 90 files changed, 427 insertions(+), 425 deletions(-)

diff --git a/src/web/app/auth/tags/index.tag b/src/web/app/auth/tags/index.tag
index e71214f4a..8d70a4162 100644
--- a/src/web/app/auth/tags/index.tag
+++ b/src/web/app/auth/tags/index.tag
@@ -114,13 +114,13 @@
 						state: 'waiting'
 					});
 
-					this.refs.form.on('denied', () => {
+					this.$refs.form.on('denied', () => {
 						this.update({
 							state: 'denied'
 						});
 					});
 
-					this.refs.form.on('accepted', this.accepted);
+					this.$refs.form.on('accepted', this.accepted);
 				}
 			}).catch(error => {
 				this.update({
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 7e76778f9..fec542500 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -289,7 +289,7 @@
 		this.files = null;
 
 		this.on('mount', () => {
-			this.refs.uploader.on('uploaded', file => {
+			this.$refs.uploader.on('uploaded', file => {
 				this.update({
 					files: [file]
 				});
@@ -297,7 +297,7 @@
 		});
 
 		this.upload = file => {
-			this.refs.uploader.upload(file);
+			this.$refs.uploader.upload(file);
 		};
 
 		this.clearReply = () => {
@@ -311,7 +311,7 @@
 			this.update({
 				files: null
 			});
-			this.refs.text.value = '';
+			this.$refs.text.value = '';
 		};
 
 		this.post = () => {
@@ -324,7 +324,7 @@
 				: undefined;
 
 			this.api('posts/create', {
-				text: this.refs.text.value == '' ? undefined : this.refs.text.value,
+				text: this.$refs.text.value == '' ? undefined : this.$refs.text.value,
 				media_ids: files,
 				reply_id: this.reply ? this.reply.id : undefined,
 				channel_id: this.channel.id
@@ -340,11 +340,11 @@
 		};
 
 		this.changeFile = () => {
-			Array.from(this.refs.file.files).forEach(this.upload);
+			Array.from(this.$refs.file.files).forEach(this.upload);
 		};
 
 		this.selectFile = () => {
-			this.refs.file.click();
+			this.$refs.file.click();
 		};
 
 		this.drive = () => {
diff --git a/src/web/app/common/tags/messaging/form.tag b/src/web/app/common/tags/messaging/form.tag
index a5de32e3f..93733e8d7 100644
--- a/src/web/app/common/tags/messaging/form.tag
+++ b/src/web/app/common/tags/messaging/form.tag
@@ -136,7 +136,7 @@
 		};
 
 		this.selectFile = () => {
-			this.refs.file.click();
+			this.$refs.file.click();
 		};
 
 		this.selectFileFromDrive = () => {
@@ -155,7 +155,7 @@
 			this.sending = true;
 			this.api('messaging/messages/create', {
 				user_id: this.opts.user.id,
-				text: this.refs.text.value
+				text: this.$refs.text.value
 			}).then(message => {
 				this.clear();
 			}).catch(err => {
@@ -167,7 +167,7 @@
 		};
 
 		this.clear = () => {
-			this.refs.text.value = '';
+			this.$refs.text.value = '';
 			this.files = [];
 			this.update();
 		};
diff --git a/src/web/app/common/tags/messaging/index.tag b/src/web/app/common/tags/messaging/index.tag
index 547727da2..d38569999 100644
--- a/src/web/app/common/tags/messaging/index.tag
+++ b/src/web/app/common/tags/messaging/index.tag
@@ -389,7 +389,7 @@
 		};
 
 		this.search = () => {
-			const q = this.refs.search.value;
+			const q = this.$refs.search.value;
 			if (q == '') {
 				this.searchResult = [];
 				return;
@@ -416,7 +416,7 @@
 				case 40: // [↓]
 					e.preventDefault();
 					e.stopPropagation();
-					this.refs.searchResult.childNodes[0].focus();
+					this.$refs.searchResult.childNodes[0].focus();
 					break;
 			}
 		};
@@ -435,19 +435,19 @@
 
 				case e.which == 27: // [ESC]
 					cancel();
-					this.refs.search.focus();
+					this.$refs.search.focus();
 					break;
 
 				case e.which == 9 && e.shiftKey: // [TAB] + [Shift]
 				case e.which == 38: // [↑]
 					cancel();
-					(this.refs.searchResult.childNodes[i].previousElementSibling || this.refs.searchResult.childNodes[this.searchResult.length - 1]).focus();
+					(this.$refs.searchResult.childNodes[i].previousElementSibling || this.$refs.searchResult.childNodes[this.searchResult.length - 1]).focus();
 					break;
 
 				case e.which == 9: // [TAB]
 				case e.which == 40: // [↓]
 					cancel();
-					(this.refs.searchResult.childNodes[i].nextElementSibling || this.refs.searchResult.childNodes[0]).focus();
+					(this.$refs.searchResult.childNodes[i].nextElementSibling || this.$refs.searchResult.childNodes[0]).focus();
 					break;
 			}
 		};
diff --git a/src/web/app/common/tags/messaging/message.tag b/src/web/app/common/tags/messaging/message.tag
index 354022d7d..f211b10b5 100644
--- a/src/web/app/common/tags/messaging/message.tag
+++ b/src/web/app/common/tags/messaging/message.tag
@@ -217,9 +217,9 @@
 			if (this.message.text) {
 				const tokens = this.message.ast;
 
-				this.refs.text.innerHTML = compile(tokens);
+				this.$refs.text.innerHTML = compile(tokens);
 
-				Array.from(this.refs.text.children).forEach(e => {
+				Array.from(this.$refs.text.children).forEach(e => {
 					if (e.tagName == 'MK-URL') riot.mount(e);
 				});
 
@@ -227,7 +227,7 @@
 				tokens
 					.filter(t => t.type == 'link')
 					.map(t => {
-						const el = this.refs.text.appendChild(document.createElement('mk-url-preview'));
+						const el = this.$refs.text.appendChild(document.createElement('mk-url-preview'));
 						riot.mount(el, {
 							url: t.content
 						});
diff --git a/src/web/app/common/tags/messaging/room.tag b/src/web/app/common/tags/messaging/room.tag
index a42e0ea94..2fdf50457 100644
--- a/src/web/app/common/tags/messaging/room.tag
+++ b/src/web/app/common/tags/messaging/room.tag
@@ -296,7 +296,7 @@
 				this.scrollToBottom();
 				n.parentNode.removeChild(n);
 			};
-			this.refs.notifications.appendChild(n);
+			this.$refs.notifications.appendChild(n);
 
 			setTimeout(() => {
 				n.style.opacity = 0;
diff --git a/src/web/app/common/tags/poll-editor.tag b/src/web/app/common/tags/poll-editor.tag
index 73e783ddb..f660032c9 100644
--- a/src/web/app/common/tags/poll-editor.tag
+++ b/src/web/app/common/tags/poll-editor.tag
@@ -95,7 +95,7 @@
 		this.add = () => {
 			this.choices.push('');
 			this.update();
-			this.refs.choices.childNodes[this.choices.length - 1].childNodes[0].focus();
+			this.$refs.choices.childNodes[this.choices.length - 1].childNodes[0].focus();
 		};
 
 		this.remove = (i) => {
diff --git a/src/web/app/common/tags/post-menu.tag b/src/web/app/common/tags/post-menu.tag
index dd2a273d4..92b2801f5 100644
--- a/src/web/app/common/tags/post-menu.tag
+++ b/src/web/app/common/tags/post-menu.tag
@@ -85,29 +85,29 @@
 
 		this.on('mount', () => {
 			const rect = this.source.getBoundingClientRect();
-			const width = this.refs.popover.offsetWidth;
-			const height = this.refs.popover.offsetHeight;
+			const width = this.$refs.popover.offsetWidth;
+			const height = this.$refs.popover.offsetHeight;
 			if (this.opts.compact) {
 				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
 				const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
-				this.refs.popover.style.left = (x - (width / 2)) + 'px';
-				this.refs.popover.style.top = (y - (height / 2)) + 'px';
+				this.$refs.popover.style.left = (x - (width / 2)) + 'px';
+				this.$refs.popover.style.top = (y - (height / 2)) + 'px';
 			} else {
 				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
 				const y = rect.top + window.pageYOffset + this.source.offsetHeight;
-				this.refs.popover.style.left = (x - (width / 2)) + 'px';
-				this.refs.popover.style.top = y + 'px';
+				this.$refs.popover.style.left = (x - (width / 2)) + 'px';
+				this.$refs.popover.style.top = y + 'px';
 			}
 
 			anime({
-				targets: this.refs.backdrop,
+				targets: this.$refs.backdrop,
 				opacity: 1,
 				duration: 100,
 				easing: 'linear'
 			});
 
 			anime({
-				targets: this.refs.popover,
+				targets: this.$refs.popover,
 				opacity: 1,
 				scale: [0.5, 1],
 				duration: 500
@@ -124,7 +124,7 @@
 		};
 
 		this.categorize = () => {
-			const category = this.refs.categorySelect.options[this.refs.categorySelect.selectedIndex].value;
+			const category = this.$refs.categorySelect.options[this.$refs.categorySelect.selectedIndex].value;
 			this.api('posts/categorize', {
 				post_id: this.post.id,
 				category: category
@@ -135,17 +135,17 @@
 		};
 
 		this.close = () => {
-			this.refs.backdrop.style.pointerEvents = 'none';
+			this.$refs.backdrop.style.pointerEvents = 'none';
 			anime({
-				targets: this.refs.backdrop,
+				targets: this.$refs.backdrop,
 				opacity: 0,
 				duration: 200,
 				easing: 'linear'
 			});
 
-			this.refs.popover.style.pointerEvents = 'none';
+			this.$refs.popover.style.pointerEvents = 'none';
 			anime({
-				targets: this.refs.popover,
+				targets: this.$refs.popover,
 				opacity: 0,
 				scale: 0.5,
 				duration: 200,
diff --git a/src/web/app/common/tags/reaction-picker.vue b/src/web/app/common/tags/reaction-picker.vue
index 970b7036d..415737208 100644
--- a/src/web/app/common/tags/reaction-picker.vue
+++ b/src/web/app/common/tags/reaction-picker.vue
@@ -25,10 +25,40 @@
 	const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%';
 
 	export default {
-		props: ['post', 'cb'],
+		props: ['post', 'source', 'compact', 'cb'],
 		data: {
 			title: placeholder
 		},
+		created: function() {
+			const rect = this.source.getBoundingClientRect();
+			const width = this.$refs.popover.offsetWidth;
+			const height = this.$refs.popover.offsetHeight;
+			if (this.compact) {
+				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+				const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
+				this.$refs.popover.style.left = (x - (width / 2)) + 'px';
+				this.$refs.popover.style.top = (y - (height / 2)) + 'px';
+			} else {
+				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+				const y = rect.top + window.pageYOffset + this.source.offsetHeight;
+				this.$refs.popover.style.left = (x - (width / 2)) + 'px';
+				this.$refs.popover.style.top = y + 'px';
+			}
+
+			anime({
+				targets: this.$refs.backdrop,
+				opacity: 1,
+				duration: 100,
+				easing: 'linear'
+			});
+
+			anime({
+				targets: this.$refs.popover,
+				opacity: 1,
+				scale: [0.5, 1],
+				duration: 500
+			});
+		},
 		methods: {
 			react: function(reaction) {
 				api('posts/reactions/create', {
@@ -54,34 +84,6 @@
 	this.source = this.opts.source;
 
 	this.on('mount', () => {
-		const rect = this.source.getBoundingClientRect();
-		const width = this.refs.popover.offsetWidth;
-		const height = this.refs.popover.offsetHeight;
-		if (this.opts.compact) {
-			const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-			const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
-			this.refs.popover.style.left = (x - (width / 2)) + 'px';
-			this.refs.popover.style.top = (y - (height / 2)) + 'px';
-		} else {
-			const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-			const y = rect.top + window.pageYOffset + this.source.offsetHeight;
-			this.refs.popover.style.left = (x - (width / 2)) + 'px';
-			this.refs.popover.style.top = y + 'px';
-		}
-
-		anime({
-			targets: this.refs.backdrop,
-			opacity: 1,
-			duration: 100,
-			easing: 'linear'
-		});
-
-		anime({
-			targets: this.refs.popover,
-			opacity: 1,
-			scale: [0.5, 1],
-			duration: 500
-		});
 	});
 
 	this.react = reaction => {
@@ -89,17 +91,17 @@
 	};
 
 	this.close = () => {
-		this.refs.backdrop.style.pointerEvents = 'none';
+		this.$refs.backdrop.style.pointerEvents = 'none';
 		anime({
-			targets: this.refs.backdrop,
+			targets: this.$refs.backdrop,
 			opacity: 0,
 			duration: 200,
 			easing: 'linear'
 		});
 
-		this.refs.popover.style.pointerEvents = 'none';
+		this.$refs.popover.style.pointerEvents = 'none';
 		anime({
-			targets: this.refs.popover,
+			targets: this.$refs.popover,
 			opacity: 0,
 			scale: 0.5,
 			duration: 200,
diff --git a/src/web/app/common/tags/signin-history.tag b/src/web/app/common/tags/signin-history.tag
index 10729789c..9f02fc687 100644
--- a/src/web/app/common/tags/signin-history.tag
+++ b/src/web/app/common/tags/signin-history.tag
@@ -104,7 +104,7 @@
 		this.show = false;
 
 		this.on('mount', () => {
-			hljs.highlightBlock(this.refs.headers);
+			hljs.highlightBlock(this.$refs.headers);
 		});
 
 		this.toggle = () => {
diff --git a/src/web/app/common/tags/signin.tag b/src/web/app/common/tags/signin.tag
index f5a2be94e..2ee188bbc 100644
--- a/src/web/app/common/tags/signin.tag
+++ b/src/web/app/common/tags/signin.tag
@@ -108,7 +108,7 @@
 
 		this.oninput = () => {
 			this.api('users/show', {
-				username: this.refs.username.value
+				username: this.$refs.username.value
 			}).then(user => {
 				this.user = user;
 				this.trigger('user', user);
@@ -119,16 +119,16 @@
 		this.onsubmit = e => {
 			e.preventDefault();
 
-			if (this.refs.username.value == '') {
-				this.refs.username.focus();
+			if (this.$refs.username.value == '') {
+				this.$refs.username.focus();
 				return false;
 			}
-			if (this.refs.password.value == '') {
-				this.refs.password.focus();
+			if (this.$refs.password.value == '') {
+				this.$refs.password.focus();
 				return false;
 			}
-			if (this.user && this.user.two_factor_enabled && this.refs.token.value == '') {
-				this.refs.token.focus();
+			if (this.user && this.user.two_factor_enabled && this.$refs.token.value == '') {
+				this.$refs.token.focus();
 				return false;
 			}
 
@@ -137,9 +137,9 @@
 			});
 
 			this.api('signin', {
-				username: this.refs.username.value,
-				password: this.refs.password.value,
-				token: this.user && this.user.two_factor_enabled ? this.refs.token.value : undefined
+				username: this.$refs.username.value,
+				password: this.$refs.password.value,
+				token: this.user && this.user.two_factor_enabled ? this.$refs.token.value : undefined
 			}).then(() => {
 				location.reload();
 			}).catch(() => {
diff --git a/src/web/app/common/tags/signup.tag b/src/web/app/common/tags/signup.tag
index d0bd76907..0b2ddf6d7 100644
--- a/src/web/app/common/tags/signup.tag
+++ b/src/web/app/common/tags/signup.tag
@@ -208,7 +208,7 @@
 		});
 
 		this.onChangeUsername = () => {
-			const username = this.refs.username.value;
+			const username = this.$refs.username.value;
 
 			if (username == '') {
 				this.update({
@@ -248,7 +248,7 @@
 		};
 
 		this.onChangePassword = () => {
-			const password = this.refs.password.value;
+			const password = this.$refs.password.value;
 
 			if (password == '') {
 				this.passwordStrength = '';
@@ -258,12 +258,12 @@
 			const strength = getPasswordStrength(password);
 			this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
 			this.update();
-			this.refs.passwordMetar.style.width = `${strength * 100}%`;
+			this.$refs.passwordMetar.style.width = `${strength * 100}%`;
 		};
 
 		this.onChangePasswordRetype = () => {
-			const password = this.refs.password.value;
-			const retypedPassword = this.refs.passwordRetype.value;
+			const password = this.$refs.password.value;
+			const retypedPassword = this.$refs.passwordRetype.value;
 
 			if (retypedPassword == '') {
 				this.passwordRetypeState = null;
@@ -276,8 +276,8 @@
 		this.onsubmit = e => {
 			e.preventDefault();
 
-			const username = this.refs.username.value;
-			const password = this.refs.password.value;
+			const username = this.$refs.username.value;
+			const password = this.$refs.password.value;
 
 			const locker = document.body.appendChild(document.createElement('mk-locker'));
 
diff --git a/src/web/app/desktop/tags/analog-clock.tag b/src/web/app/desktop/tags/analog-clock.tag
index c0489d3fe..35661405d 100644
--- a/src/web/app/desktop/tags/analog-clock.tag
+++ b/src/web/app/desktop/tags/analog-clock.tag
@@ -28,9 +28,9 @@
 			const m = now.getMinutes();
 			const h = now.getHours();
 
-			const ctx = this.refs.canvas.getContext('2d');
-			const canvW = this.refs.canvas.width;
-			const canvH = this.refs.canvas.height;
+			const ctx = this.$refs.canvas.getContext('2d');
+			const canvW = this.$refs.canvas.width;
+			const canvH = this.$refs.canvas.height;
 			ctx.clearRect(0, 0, canvW, canvH);
 
 			{ // 背景
diff --git a/src/web/app/desktop/tags/autocomplete-suggestion.tag b/src/web/app/desktop/tags/autocomplete-suggestion.tag
index 5304875c1..cf22f3a27 100644
--- a/src/web/app/desktop/tags/autocomplete-suggestion.tag
+++ b/src/web/app/desktop/tags/autocomplete-suggestion.tag
@@ -177,12 +177,12 @@
 		};
 
 		this.applySelect = () => {
-			Array.from(this.refs.users.children).forEach(el => {
+			Array.from(this.$refs.users.children).forEach(el => {
 				el.removeAttribute('data-selected');
 			});
 
-			this.refs.users.children[this.select].setAttribute('data-selected', 'true');
-			this.refs.users.children[this.select].focus();
+			this.$refs.users.children[this.select].setAttribute('data-selected', 'true');
+			this.$refs.users.children[this.select].focus();
 		};
 
 		this.complete = user => {
diff --git a/src/web/app/desktop/tags/crop-window.tag b/src/web/app/desktop/tags/crop-window.tag
index b74b46b77..80f3f4657 100644
--- a/src/web/app/desktop/tags/crop-window.tag
+++ b/src/web/app/desktop/tags/crop-window.tag
@@ -168,7 +168,7 @@
 		this.cropper = null;
 
 		this.on('mount', () => {
-			this.img = this.refs.window.refs.img;
+			this.img = this.$refs.window.refs.img;
 			this.cropper = new Cropper(this.img, {
 				aspectRatio: this.aspectRatio,
 				highlight: false,
@@ -179,18 +179,18 @@
 		this.ok = () => {
 			this.cropper.getCroppedCanvas().toBlob(blob => {
 				this.trigger('cropped', blob);
-				this.refs.window.close();
+				this.$refs.window.close();
 			});
 		};
 
 		this.skip = () => {
 			this.trigger('skipped');
-			this.refs.window.close();
+			this.$refs.window.close();
 		};
 
 		this.cancel = () => {
 			this.trigger('canceled');
-			this.refs.window.close();
+			this.$refs.window.close();
 		};
 	</script>
 </mk-crop-window>
diff --git a/src/web/app/desktop/tags/detailed-post-window.tag b/src/web/app/desktop/tags/detailed-post-window.tag
index a0bcdc79a..93df377c4 100644
--- a/src/web/app/desktop/tags/detailed-post-window.tag
+++ b/src/web/app/desktop/tags/detailed-post-window.tag
@@ -62,8 +62,8 @@
 		});
 
 		this.close = () => {
-			this.refs.bg.style.pointerEvents = 'none';
-			this.refs.main.style.pointerEvents = 'none';
+			this.$refs.bg.style.pointerEvents = 'none';
+			this.$refs.main.style.pointerEvents = 'none';
 			anime({
 				targets: this.root,
 				opacity: 0,
diff --git a/src/web/app/desktop/tags/dialog.tag b/src/web/app/desktop/tags/dialog.tag
index f21321173..9299e9733 100644
--- a/src/web/app/desktop/tags/dialog.tag
+++ b/src/web/app/desktop/tags/dialog.tag
@@ -94,19 +94,19 @@
 		});
 
 		this.on('mount', () => {
-			this.refs.header.innerHTML = this.opts.title;
-			this.refs.body.innerHTML = this.opts.text;
+			this.$refs.header.innerHTML = this.opts.title;
+			this.$refs.body.innerHTML = this.opts.text;
 
-			this.refs.bg.style.pointerEvents = 'auto';
+			this.$refs.bg.style.pointerEvents = 'auto';
 			anime({
-				targets: this.refs.bg,
+				targets: this.$refs.bg,
 				opacity: 1,
 				duration: 100,
 				easing: 'linear'
 			});
 
 			anime({
-				targets: this.refs.main,
+				targets: this.$refs.main,
 				opacity: 1,
 				scale: [1.2, 1],
 				duration: 300,
@@ -115,17 +115,17 @@
 		});
 
 		this.close = () => {
-			this.refs.bg.style.pointerEvents = 'none';
+			this.$refs.bg.style.pointerEvents = 'none';
 			anime({
-				targets: this.refs.bg,
+				targets: this.$refs.bg,
 				opacity: 0,
 				duration: 300,
 				easing: 'linear'
 			});
 
-			this.refs.main.style.pointerEvents = 'none';
+			this.$refs.main.style.pointerEvents = 'none';
 			anime({
-				targets: this.refs.main,
+				targets: this.$refs.main,
 				opacity: 0,
 				scale: 0.8,
 				duration: 300,
diff --git a/src/web/app/desktop/tags/drive/base-contextmenu.tag b/src/web/app/desktop/tags/drive/base-contextmenu.tag
index 2d7796c68..eb97ccccc 100644
--- a/src/web/app/desktop/tags/drive/base-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/base-contextmenu.tag
@@ -16,29 +16,29 @@
 		this.browser = this.opts.browser;
 
 		this.on('mount', () => {
-			this.refs.ctx.on('closed', () => {
+			this.$refs.ctx.on('closed', () => {
 				this.trigger('closed');
 				this.unmount();
 			});
 		});
 
 		this.open = pos => {
-			this.refs.ctx.open(pos);
+			this.$refs.ctx.open(pos);
 		};
 
 		this.createFolder = () => {
 			this.browser.createFolder();
-			this.refs.ctx.close();
+			this.$refs.ctx.close();
 		};
 
 		this.upload = () => {
 			this.browser.selectLocalFile();
-			this.refs.ctx.close();
+			this.$refs.ctx.close();
 		};
 
 		this.urlUpload = () => {
 			this.browser.urlUpload();
-			this.refs.ctx.close();
+			this.$refs.ctx.close();
 		};
 	</script>
 </mk-drive-browser-base-contextmenu>
diff --git a/src/web/app/desktop/tags/drive/browser-window.tag b/src/web/app/desktop/tags/drive/browser-window.tag
index 57042f016..01cb4b1af 100644
--- a/src/web/app/desktop/tags/drive/browser-window.tag
+++ b/src/web/app/desktop/tags/drive/browser-window.tag
@@ -33,7 +33,7 @@
 		this.folder = this.opts.folder ? this.opts.folder : null;
 
 		this.popout = () => {
-			const folder = this.refs.window.refs.browser.folder;
+			const folder = this.$refs.window.refs.browser.folder;
 			if (folder) {
 				return `${_URL_}/i/drive/folder/${folder.id}`;
 			} else {
@@ -42,7 +42,7 @@
 		};
 
 		this.on('mount', () => {
-			this.refs.window.on('closed', () => {
+			this.$refs.window.on('closed', () => {
 				this.unmount();
 			});
 
@@ -54,7 +54,7 @@
 		});
 
 		this.close = () => {
-			this.refs.window.close();
+			this.$refs.window.close();
 		};
 	</script>
 </mk-drive-browser-window>
diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/tags/drive/browser.tag
index f9dea5127..7e9f4662f 100644
--- a/src/web/app/desktop/tags/drive/browser.tag
+++ b/src/web/app/desktop/tags/drive/browser.tag
@@ -275,11 +275,11 @@
 		this.isDragSource = false;
 
 		this.on('mount', () => {
-			this.refs.uploader.on('uploaded', file => {
+			this.$refs.uploader.on('uploaded', file => {
 				this.addFile(file, true);
 			});
 
-			this.refs.uploader.on('change-uploads', uploads => {
+			this.$refs.uploader.on('change-uploads', uploads => {
 				this.update({
 					uploads: uploads
 				});
@@ -332,35 +332,35 @@
 		};
 
 		this.onmousedown = e => {
-			if (contains(this.refs.foldersContainer, e.target) || contains(this.refs.filesContainer, e.target)) return true;
+			if (contains(this.$refs.foldersContainer, e.target) || contains(this.$refs.filesContainer, e.target)) return true;
 
-			const rect = this.refs.main.getBoundingClientRect();
+			const rect = this.$refs.main.getBoundingClientRect();
 
-			const left = e.pageX + this.refs.main.scrollLeft - rect.left - window.pageXOffset
-			const top = e.pageY + this.refs.main.scrollTop - rect.top - window.pageYOffset
+			const left = e.pageX + this.$refs.main.scrollLeft - rect.left - window.pageXOffset
+			const top = e.pageY + this.$refs.main.scrollTop - rect.top - window.pageYOffset
 
 			const move = e => {
-				this.refs.selection.style.display = 'block';
+				this.$refs.selection.style.display = 'block';
 
-				const cursorX = e.pageX + this.refs.main.scrollLeft - rect.left - window.pageXOffset;
-				const cursorY = e.pageY + this.refs.main.scrollTop - rect.top - window.pageYOffset;
+				const cursorX = e.pageX + this.$refs.main.scrollLeft - rect.left - window.pageXOffset;
+				const cursorY = e.pageY + this.$refs.main.scrollTop - rect.top - window.pageYOffset;
 				const w = cursorX - left;
 				const h = cursorY - top;
 
 				if (w > 0) {
-					this.refs.selection.style.width = w + 'px';
-					this.refs.selection.style.left = left + 'px';
+					this.$refs.selection.style.width = w + 'px';
+					this.$refs.selection.style.left = left + 'px';
 				} else {
-					this.refs.selection.style.width = -w + 'px';
-					this.refs.selection.style.left = cursorX + 'px';
+					this.$refs.selection.style.width = -w + 'px';
+					this.$refs.selection.style.left = cursorX + 'px';
 				}
 
 				if (h > 0) {
-					this.refs.selection.style.height = h + 'px';
-					this.refs.selection.style.top = top + 'px';
+					this.$refs.selection.style.height = h + 'px';
+					this.$refs.selection.style.top = top + 'px';
 				} else {
-					this.refs.selection.style.height = -h + 'px';
-					this.refs.selection.style.top = cursorY + 'px';
+					this.$refs.selection.style.height = -h + 'px';
+					this.$refs.selection.style.top = cursorY + 'px';
 				}
 			};
 
@@ -368,7 +368,7 @@
 				document.documentElement.removeEventListener('mousemove', move);
 				document.documentElement.removeEventListener('mouseup', up);
 
-				this.refs.selection.style.display = 'none';
+				this.$refs.selection.style.display = 'none';
 			};
 
 			document.documentElement.addEventListener('mousemove', move);
@@ -482,7 +482,7 @@
 		};
 
 		this.selectLocalFile = () => {
-			this.refs.fileInput.click();
+			this.$refs.fileInput.click();
 		};
 
 		this.urlUpload = () => {
@@ -516,14 +516,14 @@
 		};
 
 		this.changeFileInput = () => {
-			Array.from(this.refs.fileInput.files).forEach(file => {
+			Array.from(this.$refs.fileInput.files).forEach(file => {
 				this.upload(file, this.folder);
 			});
 		};
 
 		this.upload = (file, folder) => {
 			if (folder && typeof folder == 'object') folder = folder.id;
-			this.refs.uploader.upload(file, folder);
+			this.$refs.uploader.upload(file, folder);
 		};
 
 		this.chooseFile = file => {
diff --git a/src/web/app/desktop/tags/drive/file-contextmenu.tag b/src/web/app/desktop/tags/drive/file-contextmenu.tag
index 31ab05c23..25721372b 100644
--- a/src/web/app/desktop/tags/drive/file-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/file-contextmenu.tag
@@ -48,18 +48,18 @@
 		this.file = this.opts.file;
 
 		this.on('mount', () => {
-			this.refs.ctx.on('closed', () => {
+			this.$refs.ctx.on('closed', () => {
 				this.trigger('closed');
 				this.unmount();
 			});
 		});
 
 		this.open = pos => {
-			this.refs.ctx.open(pos);
+			this.$refs.ctx.open(pos);
 		};
 
 		this.rename = () => {
-			this.refs.ctx.close();
+			this.$refs.ctx.close();
 
 			inputDialog('%i18n:desktop.tags.mk-drive-browser-file-contextmenu.rename-file%', '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.input-new-file-name%', this.file.name, name => {
 				this.api('drive/files/update', {
@@ -71,7 +71,7 @@
 
 		this.copyUrl = () => {
 			copyToClipboard(this.file.url);
-			this.refs.ctx.close();
+			this.$refs.ctx.close();
 			dialog('%fa:check%%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copied%',
 				'%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copied-url-to-clipboard%', [{
 				text: '%i18n:common.ok%'
@@ -79,16 +79,16 @@
 		};
 
 		this.download = () => {
-			this.refs.ctx.close();
+			this.$refs.ctx.close();
 		};
 
 		this.setAvatar = () => {
-			this.refs.ctx.close();
+			this.$refs.ctx.close();
 			updateAvatar(this.I, null, this.file);
 		};
 
 		this.setBanner = () => {
-			this.refs.ctx.close();
+			this.$refs.ctx.close();
 			updateBanner(this.I, null, this.file);
 		};
 
diff --git a/src/web/app/desktop/tags/drive/file.tag b/src/web/app/desktop/tags/drive/file.tag
index 2a1519dc7..467768db1 100644
--- a/src/web/app/desktop/tags/drive/file.tag
+++ b/src/web/app/desktop/tags/drive/file.tag
@@ -206,7 +206,7 @@
 		this.onload = () => {
 			if (this.file.properties.average_color) {
 				anime({
-					targets: this.refs.thumbnail,
+					targets: this.$refs.thumbnail,
 					backgroundColor: `rgba(${this.file.properties.average_color.join(',')}, 0)`,
 					duration: 100,
 					easing: 'linear'
diff --git a/src/web/app/desktop/tags/drive/folder-contextmenu.tag b/src/web/app/desktop/tags/drive/folder-contextmenu.tag
index eb8cad52a..d424482fa 100644
--- a/src/web/app/desktop/tags/drive/folder-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/folder-contextmenu.tag
@@ -26,9 +26,9 @@
 		this.folder = this.opts.folder;
 
 		this.open = pos => {
-			this.refs.ctx.open(pos);
+			this.$refs.ctx.open(pos);
 
-			this.refs.ctx.on('closed', () => {
+			this.$refs.ctx.on('closed', () => {
 				this.trigger('closed');
 				this.unmount();
 			});
@@ -36,21 +36,21 @@
 
 		this.move = () => {
 			this.browser.move(this.folder.id);
-			this.refs.ctx.close();
+			this.$refs.ctx.close();
 		};
 
 		this.newWindow = () => {
 			this.browser.newWindow(this.folder.id);
-			this.refs.ctx.close();
+			this.$refs.ctx.close();
 		};
 
 		this.createFolder = () => {
 			this.browser.createFolder();
-			this.refs.ctx.close();
+			this.$refs.ctx.close();
 		};
 
 		this.rename = () => {
-			this.refs.ctx.close();
+			this.$refs.ctx.close();
 
 			inputDialog('%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.rename-folder%', '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.input-new-folder-name%', this.folder.name, name => {
 				this.api('drive/folders/update', {
diff --git a/src/web/app/desktop/tags/home-widgets/access-log.tag b/src/web/app/desktop/tags/home-widgets/access-log.tag
index 91a71022a..ecf121d58 100644
--- a/src/web/app/desktop/tags/home-widgets/access-log.tag
+++ b/src/web/app/desktop/tags/home-widgets/access-log.tag
@@ -84,7 +84,7 @@
 			if (this.requests.length > 30) this.requests.shift();
 			this.update();
 
-			this.refs.log.scrollTop = this.refs.log.scrollHeight;
+			this.$refs.log.scrollTop = this.$refs.log.scrollHeight;
 		};
 
 		this.func = () => {
diff --git a/src/web/app/desktop/tags/home-widgets/activity.tag b/src/web/app/desktop/tags/home-widgets/activity.tag
index 2274e8416..f2e9cf824 100644
--- a/src/web/app/desktop/tags/home-widgets/activity.tag
+++ b/src/web/app/desktop/tags/home-widgets/activity.tag
@@ -15,7 +15,7 @@
 		this.initializing = true;
 
 		this.on('mount', () => {
-			this.refs.activity.on('view-changed', view => {
+			this.$refs.activity.on('view-changed', view => {
 				this.data.view = view;
 				this.save();
 			});
@@ -23,7 +23,7 @@
 
 		this.func = () => {
 			if (++this.data.design == 3) this.data.design = 0;
-			this.refs.activity.update({
+			this.$refs.activity.update({
 				design: this.data.design
 			});
 			this.save();
diff --git a/src/web/app/desktop/tags/home-widgets/channel.tag b/src/web/app/desktop/tags/home-widgets/channel.tag
index 0e40caa1e..c51ca0752 100644
--- a/src/web/app/desktop/tags/home-widgets/channel.tag
+++ b/src/web/app/desktop/tags/home-widgets/channel.tag
@@ -82,7 +82,7 @@
 					channel: channel
 				});
 
-				this.refs.channel.zap(channel);
+				this.$refs.channel.zap(channel);
 			});
 		};
 
@@ -185,7 +185,7 @@
 		};
 
 		this.scrollToBottom = () => {
-			this.refs.posts.scrollTop = this.refs.posts.scrollHeight;
+			this.$refs.posts.scrollTop = this.$refs.posts.scrollHeight;
 		};
 	</script>
 </mk-channel>
@@ -279,7 +279,7 @@
 		this.mixin('api');
 
 		this.clear = () => {
-			this.refs.text.value = '';
+			this.$refs.text.value = '';
 		};
 
 		this.onkeydown = e => {
@@ -291,7 +291,7 @@
 				wait: true
 			});
 
-			let text = this.refs.text.value;
+			let text = this.$refs.text.value;
 			let reply = null;
 
 			if (/^>>([0-9]+) /.test(text)) {
diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/tags/home-widgets/mentions.tag
index 94329f030..5177b2db1 100644
--- a/src/web/app/desktop/tags/home-widgets/mentions.tag
+++ b/src/web/app/desktop/tags/home-widgets/mentions.tag
@@ -76,7 +76,7 @@
 		this.onDocumentKeydown = e => {
 			if (e.target.tagName != 'INPUT' && tag != 'TEXTAREA') {
 				if (e.which == 84) { // t
-					this.refs.timeline.focus();
+					this.$refs.timeline.focus();
 				}
 			}
 		};
@@ -89,24 +89,24 @@
 					isLoading: false,
 					isEmpty: posts.length == 0
 				});
-				this.refs.timeline.setPosts(posts);
+				this.$refs.timeline.setPosts(posts);
 				if (cb) cb();
 			});
 		};
 
 		this.more = () => {
-			if (this.moreLoading || this.isLoading || this.refs.timeline.posts.length == 0) return;
+			if (this.moreLoading || this.isLoading || this.$refs.timeline.posts.length == 0) return;
 			this.update({
 				moreLoading: true
 			});
 			this.api('posts/mentions', {
 				following: this.mode == 'following',
-				until_id: this.refs.timeline.tail().id
+				until_id: this.$refs.timeline.tail().id
 			}).then(posts => {
 				this.update({
 					moreLoading: false
 				});
-				this.refs.timeline.prependPosts(posts);
+				this.$refs.timeline.prependPosts(posts);
 			});
 		};
 
diff --git a/src/web/app/desktop/tags/home-widgets/messaging.tag b/src/web/app/desktop/tags/home-widgets/messaging.tag
index f2c7c3589..695e1babf 100644
--- a/src/web/app/desktop/tags/home-widgets/messaging.tag
+++ b/src/web/app/desktop/tags/home-widgets/messaging.tag
@@ -37,7 +37,7 @@
 		this.mixin('widget');
 
 		this.on('mount', () => {
-			this.refs.index.on('navigate-user', user => {
+			this.$refs.index.on('navigate-user', user => {
 				riot.mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
 					user: user
 				});
diff --git a/src/web/app/desktop/tags/home-widgets/post-form.tag b/src/web/app/desktop/tags/home-widgets/post-form.tag
index b6310d6aa..bf6374dd3 100644
--- a/src/web/app/desktop/tags/home-widgets/post-form.tag
+++ b/src/web/app/desktop/tags/home-widgets/post-form.tag
@@ -84,7 +84,7 @@
 			});
 
 			this.api('posts/create', {
-				text: this.refs.text.value
+				text: this.$refs.text.value
 			}).then(data => {
 				this.clear();
 			}).catch(err => {
@@ -97,7 +97,7 @@
 		};
 
 		this.clear = () => {
-			this.refs.text.value = '';
+			this.$refs.text.value = '';
 		};
 	</script>
 </mk-post-form-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/server.tag b/src/web/app/desktop/tags/home-widgets/server.tag
index 6eb4ce15b..6749a46b1 100644
--- a/src/web/app/desktop/tags/home-widgets/server.tag
+++ b/src/web/app/desktop/tags/home-widgets/server.tag
@@ -284,7 +284,7 @@
 		});
 
 		this.onStats = stats => {
-			this.refs.pie.render(stats.cpu_usage);
+			this.$refs.pie.render(stats.cpu_usage);
 		};
 	</script>
 </mk-server-home-widget-cpu>
@@ -344,7 +344,7 @@
 
 		this.onStats = stats => {
 			stats.mem.used = stats.mem.total - stats.mem.free;
-			this.refs.pie.render(stats.mem.used / stats.mem.total);
+			this.$refs.pie.render(stats.mem.used / stats.mem.total);
 
 			this.update({
 				total: stats.mem.total,
@@ -411,7 +411,7 @@
 		this.onStats = stats => {
 			stats.disk.used = stats.disk.total - stats.disk.free;
 
-			this.refs.pie.render(stats.disk.used / stats.disk.total);
+			this.$refs.pie.render(stats.disk.used / stats.disk.total);
 
 			this.update({
 				total: stats.disk.total,
diff --git a/src/web/app/desktop/tags/home-widgets/slideshow.tag b/src/web/app/desktop/tags/home-widgets/slideshow.tag
index af54fd893..21b778bae 100644
--- a/src/web/app/desktop/tags/home-widgets/slideshow.tag
+++ b/src/web/app/desktop/tags/home-widgets/slideshow.tag
@@ -101,17 +101,17 @@
 			const index = Math.floor(Math.random() * this.images.length);
 			const img = `url(${ this.images[index].url }?thumbnail&size=1024)`;
 
-			this.refs.slideB.style.backgroundImage = img;
+			this.$refs.slideB.style.backgroundImage = img;
 
 			anime({
-				targets: this.refs.slideB,
+				targets: this.$refs.slideB,
 				opacity: 1,
 				duration: 1000,
 				easing: 'linear',
 				complete: () => {
-					this.refs.slideA.style.backgroundImage = img;
+					this.$refs.slideA.style.backgroundImage = img;
 					anime({
-						targets: this.refs.slideB,
+						targets: this.$refs.slideB,
 						opacity: 0,
 						duration: 0
 					});
@@ -133,8 +133,8 @@
 					fetching: false,
 					images: images
 				});
-				this.refs.slideA.style.backgroundImage = '';
-				this.refs.slideB.style.backgroundImage = '';
+				this.$refs.slideA.style.backgroundImage = '';
+				this.$refs.slideB.style.backgroundImage = '';
 				this.change();
 			});
 		};
diff --git a/src/web/app/desktop/tags/home-widgets/timeline.tag b/src/web/app/desktop/tags/home-widgets/timeline.tag
index 9571b09f3..f44023daa 100644
--- a/src/web/app/desktop/tags/home-widgets/timeline.tag
+++ b/src/web/app/desktop/tags/home-widgets/timeline.tag
@@ -75,7 +75,7 @@
 		this.onDocumentKeydown = e => {
 			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
 				if (e.which == 84) { // t
-					this.refs.timeline.focus();
+					this.$refs.timeline.focus();
 				}
 			}
 		};
@@ -92,23 +92,23 @@
 					isLoading: false,
 					isEmpty: posts.length == 0
 				});
-				this.refs.timeline.setPosts(posts);
+				this.$refs.timeline.setPosts(posts);
 				if (cb) cb();
 			});
 		};
 
 		this.more = () => {
-			if (this.moreLoading || this.isLoading || this.refs.timeline.posts.length == 0) return;
+			if (this.moreLoading || this.isLoading || this.$refs.timeline.posts.length == 0) return;
 			this.update({
 				moreLoading: true
 			});
 			this.api('posts/timeline', {
-				until_id: this.refs.timeline.tail().id
+				until_id: this.$refs.timeline.tail().id
 			}).then(posts => {
 				this.update({
 					moreLoading: false
 				});
-				this.refs.timeline.prependPosts(posts);
+				this.$refs.timeline.prependPosts(posts);
 			});
 		};
 
@@ -116,7 +116,7 @@
 			this.update({
 				isEmpty: false
 			});
-			this.refs.timeline.addPost(post);
+			this.$refs.timeline.addPost(post);
 		};
 
 		this.onStreamFollow = () => {
diff --git a/src/web/app/desktop/tags/home-widgets/tips.tag b/src/web/app/desktop/tags/home-widgets/tips.tag
index 53b61dca9..9246d0e10 100644
--- a/src/web/app/desktop/tags/home-widgets/tips.tag
+++ b/src/web/app/desktop/tags/home-widgets/tips.tag
@@ -69,12 +69,12 @@
 		});
 
 		this.set = () => {
-			this.refs.text.innerHTML = this.tips[Math.floor(Math.random() * this.tips.length)];
+			this.$refs.text.innerHTML = this.tips[Math.floor(Math.random() * this.tips.length)];
 		};
 
 		this.change = () => {
 			anime({
-				targets: this.refs.tip,
+				targets: this.$refs.tip,
 				opacity: 0,
 				duration: 500,
 				easing: 'linear',
@@ -83,7 +83,7 @@
 
 			setTimeout(() => {
 				anime({
-					targets: this.refs.tip,
+					targets: this.$refs.tip,
 					opacity: 1,
 					duration: 500,
 					easing: 'linear'
diff --git a/src/web/app/desktop/tags/home.tag b/src/web/app/desktop/tags/home.tag
index 90486f7d2..204760796 100644
--- a/src/web/app/desktop/tags/home.tag
+++ b/src/web/app/desktop/tags/home.tag
@@ -197,7 +197,7 @@
 		this.bakedHomeData = this.bakeHomeData();
 
 		this.on('mount', () => {
-			this.refs.tl.on('loaded', () => {
+			this.$refs.tl.on('loaded', () => {
 				this.trigger('loaded');
 			});
 
@@ -212,11 +212,11 @@
 			});
 
 			if (!this.opts.customize) {
-				if (this.refs.left.children.length == 0) {
-					this.refs.left.parentNode.removeChild(this.refs.left);
+				if (this.$refs.left.children.length == 0) {
+					this.$refs.left.parentNode.removeChild(this.$refs.left);
 				}
-				if (this.refs.right.children.length == 0) {
-					this.refs.right.parentNode.removeChild(this.refs.right);
+				if (this.$refs.right.children.length == 0) {
+					this.$refs.right.parentNode.removeChild(this.$refs.right);
 				}
 			}
 
@@ -242,10 +242,10 @@
 					}
 				};
 
-				new Sortable(this.refs.left, sortableOption);
-				new Sortable(this.refs.right, sortableOption);
-				new Sortable(this.refs.maintop, sortableOption);
-				new Sortable(this.refs.trash, Object.assign({}, sortableOption, {
+				new Sortable(this.$refs.left, sortableOption);
+				new Sortable(this.$refs.right, sortableOption);
+				new Sortable(this.$refs.maintop, sortableOption);
+				new Sortable(this.$refs.trash, Object.assign({}, sortableOption, {
 					onAdd: evt => {
 						const el = evt.item;
 						const id = el.getAttribute('data-widget-id');
@@ -257,8 +257,8 @@
 			}
 
 			if (!this.opts.customize) {
-				this.scrollFollowerLeft = this.refs.left.parentNode ? new ScrollFollower(this.refs.left, this.root.getBoundingClientRect().top) : null;
-				this.scrollFollowerRight = this.refs.right.parentNode ? new ScrollFollower(this.refs.right, this.root.getBoundingClientRect().top) : null;
+				this.scrollFollowerLeft = this.$refs.left.parentNode ? new ScrollFollower(this.$refs.left, this.root.getBoundingClientRect().top) : null;
+				this.scrollFollowerRight = this.$refs.right.parentNode ? new ScrollFollower(this.$refs.right, this.root.getBoundingClientRect().top) : null;
 			}
 		});
 
@@ -299,23 +299,23 @@
 			switch (widget.place) {
 				case 'left':
 					if (prepend) {
-						this.refs.left.insertBefore(actualEl, this.refs.left.firstChild);
+						this.$refs.left.insertBefore(actualEl, this.$refs.left.firstChild);
 					} else {
-						this.refs.left.appendChild(actualEl);
+						this.$refs.left.appendChild(actualEl);
 					}
 					break;
 				case 'right':
 					if (prepend) {
-						this.refs.right.insertBefore(actualEl, this.refs.right.firstChild);
+						this.$refs.right.insertBefore(actualEl, this.$refs.right.firstChild);
 					} else {
-						this.refs.right.appendChild(actualEl);
+						this.$refs.right.appendChild(actualEl);
 					}
 					break;
 				case 'main':
 					if (this.opts.customize) {
-						this.refs.maintop.appendChild(actualEl);
+						this.$refs.maintop.appendChild(actualEl);
 					} else {
-						this.refs.main.insertBefore(actualEl, this.refs.tl.root);
+						this.$refs.main.insertBefore(actualEl, this.$refs.tl.root);
 					}
 					break;
 			}
@@ -324,7 +324,7 @@
 				id: widget.id,
 				data: widget.data,
 				place: widget.place,
-				tl: this.refs.tl
+				tl: this.$refs.tl
 			})[0];
 
 			this.home.push(tag);
@@ -341,7 +341,7 @@
 
 		this.addWidget = () => {
 			const widget = {
-				name: this.refs.widgetSelector.options[this.refs.widgetSelector.selectedIndex].value,
+				name: this.$refs.widgetSelector.options[this.$refs.widgetSelector.selectedIndex].value,
 				id: uuid(),
 				place: 'left',
 				data: {}
@@ -357,21 +357,21 @@
 		this.saveHome = () => {
 			const data = [];
 
-			Array.from(this.refs.left.children).forEach(el => {
+			Array.from(this.$refs.left.children).forEach(el => {
 				const id = el.getAttribute('data-widget-id');
 				const widget = this.I.client_settings.home.find(w => w.id == id);
 				widget.place = 'left';
 				data.push(widget);
 			});
 
-			Array.from(this.refs.right.children).forEach(el => {
+			Array.from(this.$refs.right.children).forEach(el => {
 				const id = el.getAttribute('data-widget-id');
 				const widget = this.I.client_settings.home.find(w => w.id == id);
 				widget.place = 'right';
 				data.push(widget);
 			});
 
-			Array.from(this.refs.maintop.children).forEach(el => {
+			Array.from(this.$refs.maintop.children).forEach(el => {
 				const id = el.getAttribute('data-widget-id');
 				const widget = this.I.client_settings.home.find(w => w.id == id);
 				widget.place = 'main';
diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
index 1c81af3d0..dcd664e72 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -86,17 +86,17 @@
 		};
 
 		this.mousemove = e => {
-			const rect = this.refs.view.getBoundingClientRect();
+			const rect = this.$refs.view.getBoundingClientRect();
 			const mouseX = e.clientX - rect.left;
 			const mouseY = e.clientY - rect.top;
-			const xp = mouseX / this.refs.view.offsetWidth * 100;
-			const yp = mouseY / this.refs.view.offsetHeight * 100;
-			this.refs.view.style.backgroundPosition = xp + '% ' + yp + '%';
-			this.refs.view.style.backgroundImage = 'url("' + this.image.url + '?thumbnail")';
+			const xp = mouseX / this.$refs.view.offsetWidth * 100;
+			const yp = mouseY / this.$refs.view.offsetHeight * 100;
+			this.$refs.view.style.backgroundPosition = xp + '% ' + yp + '%';
+			this.$refs.view.style.backgroundImage = 'url("' + this.image.url + '?thumbnail")';
 		};
 
 		this.mouseleave = () => {
-			this.refs.view.style.backgroundPosition = '';
+			this.$refs.view.style.backgroundPosition = '';
 		};
 
 		this.click = ev => {
diff --git a/src/web/app/desktop/tags/input-dialog.tag b/src/web/app/desktop/tags/input-dialog.tag
index 84dcedf93..bea8c2c22 100644
--- a/src/web/app/desktop/tags/input-dialog.tag
+++ b/src/web/app/desktop/tags/input-dialog.tag
@@ -129,11 +129,11 @@
 		this.type = this.opts.type ? this.opts.type : 'text';
 
 		this.on('mount', () => {
-			this.text = this.refs.window.refs.text;
+			this.text = this.$refs.window.refs.text;
 			if (this.default) this.text.value = this.default;
 			this.text.focus();
 
-			this.refs.window.on('closing', () => {
+			this.$refs.window.on('closing', () => {
 				if (this.done) {
 					this.opts.onOk(this.text.value);
 				} else {
@@ -141,20 +141,20 @@
 				}
 			});
 
-			this.refs.window.on('closed', () => {
+			this.$refs.window.on('closed', () => {
 				this.unmount();
 			});
 		});
 
 		this.cancel = () => {
 			this.done = false;
-			this.refs.window.close();
+			this.$refs.window.close();
 		};
 
 		this.ok = () => {
 			if (!this.allowEmpty && this.text.value == '') return;
 			this.done = true;
-			this.refs.window.close();
+			this.$refs.window.close();
 		};
 
 		this.onInput = () => {
diff --git a/src/web/app/desktop/tags/messaging/room-window.tag b/src/web/app/desktop/tags/messaging/room-window.tag
index 7c0bb0d76..bae456200 100644
--- a/src/web/app/desktop/tags/messaging/room-window.tag
+++ b/src/web/app/desktop/tags/messaging/room-window.tag
@@ -24,7 +24,7 @@
 		this.popout = `${_URL_}/i/messaging/${this.user.username}`;
 
 		this.on('mount', () => {
-			this.refs.window.on('closed', () => {
+			this.$refs.window.on('closed', () => {
 				this.unmount();
 			});
 		});
diff --git a/src/web/app/desktop/tags/messaging/window.tag b/src/web/app/desktop/tags/messaging/window.tag
index 529db11af..afe01c53e 100644
--- a/src/web/app/desktop/tags/messaging/window.tag
+++ b/src/web/app/desktop/tags/messaging/window.tag
@@ -20,11 +20,11 @@
 	</style>
 	<script>
 		this.on('mount', () => {
-			this.refs.window.on('closed', () => {
+			this.$refs.window.on('closed', () => {
 				this.unmount();
 			});
 
-			this.refs.window.refs.index.on('navigate-user', user => {
+			this.$refs.window.refs.index.on('navigate-user', user => {
 				riot.mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
 					user: user
 				});
diff --git a/src/web/app/desktop/tags/pages/drive.tag b/src/web/app/desktop/tags/pages/drive.tag
index 9f3e75ab2..1cd5ca127 100644
--- a/src/web/app/desktop/tags/pages/drive.tag
+++ b/src/web/app/desktop/tags/pages/drive.tag
@@ -15,7 +15,7 @@
 		this.on('mount', () => {
 			document.title = 'Misskey Drive';
 
-			this.refs.browser.on('move-root', () => {
+			this.$refs.browser.on('move-root', () => {
 				const title = 'Misskey Drive';
 
 				// Rewrite URL
@@ -24,7 +24,7 @@
 				document.title = title;
 			});
 
-			this.refs.browser.on('open-folder', folder => {
+			this.$refs.browser.on('open-folder', folder => {
 				const title = folder.name + ' | Misskey Drive';
 
 				// Rewrite URL
diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/tags/pages/entrance.tag
index d3807a1e7..95acbc910 100644
--- a/src/web/app/desktop/tags/pages/entrance.tag
+++ b/src/web/app/desktop/tags/pages/entrance.tag
@@ -280,7 +280,7 @@
 	</style>
 	<script>
 		this.on('mount', () => {
-			this.refs.signin.on('user', user => {
+			this.$refs.signin.on('user', user => {
 				this.update({
 					user: user
 				});
diff --git a/src/web/app/desktop/tags/pages/home.tag b/src/web/app/desktop/tags/pages/home.tag
index 3c8f4ec57..62df62a48 100644
--- a/src/web/app/desktop/tags/pages/home.tag
+++ b/src/web/app/desktop/tags/pages/home.tag
@@ -21,7 +21,7 @@
 		this.page = this.opts.mode || 'timeline';
 
 		this.on('mount', () => {
-			this.refs.ui.refs.home.on('loaded', () => {
+			this.$refs.ui.refs.home.on('loaded', () => {
 				Progress.done();
 			});
 			document.title = 'Misskey';
diff --git a/src/web/app/desktop/tags/pages/search.tag b/src/web/app/desktop/tags/pages/search.tag
index 4f5867bdb..ac93fdaea 100644
--- a/src/web/app/desktop/tags/pages/search.tag
+++ b/src/web/app/desktop/tags/pages/search.tag
@@ -12,7 +12,7 @@
 		this.on('mount', () => {
 			Progress.start();
 
-			this.refs.ui.refs.search.on('loaded', () => {
+			this.$refs.ui.refs.search.on('loaded', () => {
 				Progress.done();
 			});
 		});
diff --git a/src/web/app/desktop/tags/pages/selectdrive.tag b/src/web/app/desktop/tags/pages/selectdrive.tag
index 993df680f..d497a47c0 100644
--- a/src/web/app/desktop/tags/pages/selectdrive.tag
+++ b/src/web/app/desktop/tags/pages/selectdrive.tag
@@ -133,12 +133,12 @@
 		this.on('mount', () => {
 			document.title = '%i18n:desktop.tags.mk-selectdrive-page.title%';
 
-			this.refs.browser.on('selected', file => {
+			this.$refs.browser.on('selected', file => {
 				this.files = [file];
 				this.ok();
 			});
 
-			this.refs.browser.on('change-selection', files => {
+			this.$refs.browser.on('change-selection', files => {
 				this.update({
 					files: files
 				});
@@ -146,7 +146,7 @@
 		});
 
 		this.upload = () => {
-			this.refs.browser.selectLocalFile();
+			this.$refs.browser.selectLocalFile();
 		};
 
 		this.close = () => {
diff --git a/src/web/app/desktop/tags/pages/user.tag b/src/web/app/desktop/tags/pages/user.tag
index 811ca5c0f..7bad03495 100644
--- a/src/web/app/desktop/tags/pages/user.tag
+++ b/src/web/app/desktop/tags/pages/user.tag
@@ -14,12 +14,12 @@
 		this.on('mount', () => {
 			Progress.start();
 
-			this.refs.ui.refs.user.on('user-fetched', user => {
+			this.$refs.ui.refs.user.on('user-fetched', user => {
 				Progress.set(0.5);
 				document.title = user.name + ' | Misskey';
 			});
 
-			this.refs.ui.refs.user.on('loaded', () => {
+			this.$refs.ui.refs.user.on('loaded', () => {
 				Progress.done();
 			});
 		});
diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/tags/post-detail-sub.tag
index cccd85c47..2d79ddd1e 100644
--- a/src/web/app/desktop/tags/post-detail-sub.tag
+++ b/src/web/app/desktop/tags/post-detail-sub.tag
@@ -120,9 +120,9 @@
 			if (this.post.text) {
 				const tokens = this.post.ast;
 
-				this.refs.text.innerHTML = compile(tokens);
+				this.$refs.text.innerHTML = compile(tokens);
 
-				Array.from(this.refs.text.children).forEach(e => {
+				Array.from(this.$refs.text.children).forEach(e => {
 					if (e.tagName == 'MK-URL') riot.mount(e);
 				});
 			}
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index 6177f24ee..73ba930c7 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -256,9 +256,9 @@
 			if (this.p.text) {
 				const tokens = this.p.ast;
 
-				this.refs.text.innerHTML = compile(tokens);
+				this.$refs.text.innerHTML = compile(tokens);
 
-				Array.from(this.refs.text.children).forEach(e => {
+				Array.from(this.$refs.text.children).forEach(e => {
 					if (e.tagName == 'MK-URL') riot.mount(e);
 				});
 
@@ -266,7 +266,7 @@
 				tokens
 				.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
 				.map(t => {
-					riot.mount(this.refs.text.appendChild(document.createElement('mk-url-preview')), {
+					riot.mount(this.$refs.text.appendChild(document.createElement('mk-url-preview')), {
 						url: t.url
 					});
 				});
@@ -299,14 +299,14 @@
 
 		this.react = () => {
 			riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), {
-				source: this.refs.reactButton,
+				source: this.$refs.reactButton,
 				post: this.p
 			});
 		};
 
 		this.menu = () => {
 			riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
-				source: this.refs.menuButton,
+				source: this.$refs.menuButton,
 				post: this.p
 			});
 		};
diff --git a/src/web/app/desktop/tags/post-form-window.tag b/src/web/app/desktop/tags/post-form-window.tag
index 05a09b780..de349bada 100644
--- a/src/web/app/desktop/tags/post-form-window.tag
+++ b/src/web/app/desktop/tags/post-form-window.tag
@@ -42,23 +42,23 @@
 		this.files = [];
 
 		this.on('mount', () => {
-			this.refs.window.refs.form.focus();
+			this.$refs.window.refs.form.focus();
 
-			this.refs.window.on('closed', () => {
+			this.$refs.window.on('closed', () => {
 				this.unmount();
 			});
 
-			this.refs.window.refs.form.on('post', () => {
-				this.refs.window.close();
+			this.$refs.window.refs.form.on('post', () => {
+				this.$refs.window.close();
 			});
 
-			this.refs.window.refs.form.on('change-uploading-files', files => {
+			this.$refs.window.refs.form.on('change-uploading-files', files => {
 				this.update({
 					uploadingFiles: files || []
 				});
 			});
 
-			this.refs.window.refs.form.on('change-files', files => {
+			this.$refs.window.refs.form.on('change-files', files => {
 				this.update({
 					files: files || []
 				});
diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag
index 23434a824..4dbc69e4e 100644
--- a/src/web/app/desktop/tags/post-form.tag
+++ b/src/web/app/desktop/tags/post-form.tag
@@ -319,32 +319,32 @@
 				: 'post';
 
 		this.on('mount', () => {
-			this.refs.uploader.on('uploaded', file => {
+			this.$refs.uploader.on('uploaded', file => {
 				this.addFile(file);
 			});
 
-			this.refs.uploader.on('change-uploads', uploads => {
+			this.$refs.uploader.on('change-uploads', uploads => {
 				this.trigger('change-uploading-files', uploads);
 			});
 
-			this.autocomplete = new Autocomplete(this.refs.text);
+			this.autocomplete = new Autocomplete(this.$refs.text);
 			this.autocomplete.attach();
 
 			// 書きかけの投稿を復元
 			const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
 			if (draft) {
-				this.refs.text.value = draft.data.text;
+				this.$refs.text.value = draft.data.text;
 				this.files = draft.data.files;
 				if (draft.data.poll) {
 					this.poll = true;
 					this.update();
-					this.refs.poll.set(draft.data.poll);
+					this.$refs.poll.set(draft.data.poll);
 				}
 				this.trigger('change-files', this.files);
 				this.update();
 			}
 
-			new Sortable(this.refs.media, {
+			new Sortable(this.$refs.media, {
 				animation: 150
 			});
 		});
@@ -354,11 +354,11 @@
 		});
 
 		this.focus = () => {
-			this.refs.text.focus();
+			this.$refs.text.focus();
 		};
 
 		this.clear = () => {
-			this.refs.text.value = '';
+			this.$refs.text.value = '';
 			this.files = [];
 			this.poll = false;
 			this.trigger('change-files');
@@ -422,7 +422,7 @@
 		};
 
 		this.selectFile = () => {
-			this.refs.file.click();
+			this.$refs.file.click();
 		};
 
 		this.selectFileFromDrive = () => {
@@ -435,11 +435,11 @@
 		};
 
 		this.changeFile = () => {
-			Array.from(this.refs.file.files).forEach(this.upload);
+			Array.from(this.$refs.file.files).forEach(this.upload);
 		};
 
 		this.upload = file => {
-			this.refs.uploader.upload(file);
+			this.$refs.uploader.upload(file);
 		};
 
 		this.addFile = file => {
@@ -471,7 +471,7 @@
 			const files = [];
 
 			if (this.files.length > 0) {
-				Array.from(this.refs.media.children).forEach(el => {
+				Array.from(this.$refs.media.children).forEach(el => {
 					const id = el.getAttribute('data-id');
 					const file = this.files.find(f => f.id == id);
 					files.push(file);
@@ -479,11 +479,11 @@
 			}
 
 			this.api('posts/create', {
-				text: this.refs.text.value == '' ? undefined : this.refs.text.value,
+				text: this.$refs.text.value == '' ? undefined : this.$refs.text.value,
 				media_ids: this.files.length > 0 ? files.map(f => f.id) : undefined,
 				reply_id: this.inReplyToPost ? this.inReplyToPost.id : undefined,
 				repost_id: this.repost ? this.repost.id : undefined,
-				poll: this.poll ? this.refs.poll.get() : undefined
+				poll: this.poll ? this.$refs.poll.get() : undefined
 			}).then(data => {
 				this.clear();
 				this.removeDraft();
@@ -507,7 +507,7 @@
 		};
 
 		this.kao = () => {
-			this.refs.text.value += getKao();
+			this.$refs.text.value += getKao();
 		};
 
 		this.on('update', () => {
@@ -520,9 +520,9 @@
 			data[this.draftId] = {
 				updated_at: new Date(),
 				data: {
-					text: this.refs.text.value,
+					text: this.$refs.text.value,
 					files: this.files,
-					poll: this.poll && this.refs.poll ? this.refs.poll.get() : undefined
+					poll: this.poll && this.$refs.poll ? this.$refs.poll.get() : undefined
 				}
 			}
 
diff --git a/src/web/app/desktop/tags/progress-dialog.tag b/src/web/app/desktop/tags/progress-dialog.tag
index a0ac51b2f..94e7f8af4 100644
--- a/src/web/app/desktop/tags/progress-dialog.tag
+++ b/src/web/app/desktop/tags/progress-dialog.tag
@@ -78,7 +78,7 @@
 		this.max = parseInt(this.opts.max, 10);
 
 		this.on('mount', () => {
-			this.refs.window.on('closed', () => {
+			this.$refs.window.on('closed', () => {
 				this.unmount();
 			});
 		});
@@ -91,7 +91,7 @@
 		};
 
 		this.close = () => {
-			this.refs.window.close();
+			this.$refs.window.close();
 		};
 	</script>
 </mk-progress-dialog>
diff --git a/src/web/app/desktop/tags/repost-form-window.tag b/src/web/app/desktop/tags/repost-form-window.tag
index dbc3f5a3c..939ff4e38 100644
--- a/src/web/app/desktop/tags/repost-form-window.tag
+++ b/src/web/app/desktop/tags/repost-form-window.tag
@@ -19,23 +19,23 @@
 		this.onDocumentKeydown = e => {
 			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
 				if (e.which == 27) { // Esc
-					this.refs.window.close();
+					this.$refs.window.close();
 				}
 			}
 		};
 
 		this.on('mount', () => {
-			this.refs.window.refs.form.on('cancel', () => {
-				this.refs.window.close();
+			this.$refs.window.refs.form.on('cancel', () => {
+				this.$refs.window.close();
 			});
 
-			this.refs.window.refs.form.on('posted', () => {
-				this.refs.window.close();
+			this.$refs.window.refs.form.on('posted', () => {
+				this.$refs.window.close();
 			});
 
 			document.addEventListener('keydown', this.onDocumentKeydown);
 
-			this.refs.window.on('closed', () => {
+			this.$refs.window.on('closed', () => {
 				this.unmount();
 			});
 		});
diff --git a/src/web/app/desktop/tags/repost-form.tag b/src/web/app/desktop/tags/repost-form.tag
index 946871765..b2ebbf4c4 100644
--- a/src/web/app/desktop/tags/repost-form.tag
+++ b/src/web/app/desktop/tags/repost-form.tag
@@ -117,11 +117,11 @@
 				quote: true
 			});
 
-			this.refs.form.on('post', () => {
+			this.$refs.form.on('post', () => {
 				this.trigger('posted');
 			});
 
-			this.refs.form.focus();
+			this.$refs.form.focus();
 		};
 	</script>
 </mk-repost-form>
diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag
index f7ec85a4f..0c8dbcbf6 100644
--- a/src/web/app/desktop/tags/search-posts.tag
+++ b/src/web/app/desktop/tags/search-posts.tag
@@ -53,7 +53,7 @@
 					isLoading: false,
 					isEmpty: posts.length == 0
 				});
-				this.refs.timeline.setPosts(posts);
+				this.$refs.timeline.setPosts(posts);
 				this.trigger('loaded');
 			});
 		});
@@ -66,7 +66,7 @@
 		this.onDocumentKeydown = e => {
 			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
 				if (e.which == 84) { // t
-					this.refs.timeline.focus();
+					this.$refs.timeline.focus();
 				}
 			}
 		};
@@ -84,7 +84,7 @@
 				this.update({
 					moreLoading: false
 				});
-				this.refs.timeline.prependPosts(posts);
+				this.$refs.timeline.prependPosts(posts);
 			});
 		};
 
diff --git a/src/web/app/desktop/tags/search.tag b/src/web/app/desktop/tags/search.tag
index d5159fe4e..e29a2b273 100644
--- a/src/web/app/desktop/tags/search.tag
+++ b/src/web/app/desktop/tags/search.tag
@@ -26,7 +26,7 @@
 		this.query = this.opts.query;
 
 		this.on('mount', () => {
-			this.refs.posts.on('loaded', () => {
+			this.$refs.posts.on('loaded', () => {
 				this.trigger('loaded');
 			});
 		});
diff --git a/src/web/app/desktop/tags/select-file-from-drive-window.tag b/src/web/app/desktop/tags/select-file-from-drive-window.tag
index 622514558..6d1e59413 100644
--- a/src/web/app/desktop/tags/select-file-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-file-from-drive-window.tag
@@ -141,33 +141,33 @@
 		this.title = this.opts.title || '%fa:R file%ファイルを選択';
 
 		this.on('mount', () => {
-			this.refs.window.refs.browser.on('selected', file => {
+			this.$refs.window.refs.browser.on('selected', file => {
 				this.files = [file];
 				this.ok();
 			});
 
-			this.refs.window.refs.browser.on('change-selection', files => {
+			this.$refs.window.refs.browser.on('change-selection', files => {
 				this.update({
 					files: files
 				});
 			});
 
-			this.refs.window.on('closed', () => {
+			this.$refs.window.on('closed', () => {
 				this.unmount();
 			});
 		});
 
 		this.close = () => {
-			this.refs.window.close();
+			this.$refs.window.close();
 		};
 
 		this.upload = () => {
-			this.refs.window.refs.browser.selectLocalFile();
+			this.$refs.window.refs.browser.selectLocalFile();
 		};
 
 		this.ok = () => {
 			this.trigger('selected', this.multiple ? this.files : this.files[0]);
-			this.refs.window.close();
+			this.$refs.window.close();
 		};
 	</script>
 </mk-select-file-from-drive-window>
diff --git a/src/web/app/desktop/tags/select-folder-from-drive-window.tag b/src/web/app/desktop/tags/select-folder-from-drive-window.tag
index 45700420c..7bfe5af35 100644
--- a/src/web/app/desktop/tags/select-folder-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-folder-from-drive-window.tag
@@ -95,18 +95,18 @@
 		this.title = this.opts.title || '%fa:R folder%フォルダを選択';
 
 		this.on('mount', () => {
-			this.refs.window.on('closed', () => {
+			this.$refs.window.on('closed', () => {
 				this.unmount();
 			});
 		});
 
 		this.close = () => {
-			this.refs.window.close();
+			this.$refs.window.close();
 		};
 
 		this.ok = () => {
-			this.trigger('selected', this.refs.window.refs.browser.folder);
-			this.refs.window.close();
+			this.trigger('selected', this.$refs.window.refs.browser.folder);
+			this.$refs.window.close();
 		};
 	</script>
 </mk-select-folder-from-drive-window>
diff --git a/src/web/app/desktop/tags/settings-window.tag b/src/web/app/desktop/tags/settings-window.tag
index 5a725af51..e68a44a4f 100644
--- a/src/web/app/desktop/tags/settings-window.tag
+++ b/src/web/app/desktop/tags/settings-window.tag
@@ -18,13 +18,13 @@
 	</style>
 	<script>
 		this.on('mount', () => {
-			this.refs.window.on('closed', () => {
+			this.$refs.window.on('closed', () => {
 				this.unmount();
 			});
 		});
 
 		this.close = () => {
-			this.refs.window.close();
+			this.$refs.window.close();
 		};
 	</script>
 </mk-settings-window>
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index efc5da83f..084bde009 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -179,10 +179,10 @@
 
 		this.updateAccount = () => {
 			this.api('i/update', {
-				name: this.refs.accountName.value,
-				location: this.refs.accountLocation.value || null,
-				description: this.refs.accountDescription.value || null,
-				birthday: this.refs.accountBirthday.value || null
+				name: this.$refs.accountName.value,
+				location: this.$refs.accountLocation.value || null,
+				description: this.$refs.accountDescription.value || null,
+				birthday: this.$refs.accountBirthday.value || null
 			}).then(() => {
 				notify('プロフィールを更新しました');
 			});
@@ -320,7 +320,7 @@
 
 		this.submit = () => {
 			this.api('i/2fa/done', {
-				token: this.refs.token.value
+				token: this.$refs.token.value
 			}).then(() => {
 				notify('%i18n:desktop.tags.mk-2fa-setting.success%');
 				this.I.two_factor_enabled = true;
diff --git a/src/web/app/desktop/tags/sub-post-content.tag b/src/web/app/desktop/tags/sub-post-content.tag
index 1a81b545b..01e1fdb31 100644
--- a/src/web/app/desktop/tags/sub-post-content.tag
+++ b/src/web/app/desktop/tags/sub-post-content.tag
@@ -43,9 +43,9 @@
 		this.on('mount', () => {
 			if (this.post.text) {
 				const tokens = this.post.ast;
-				this.refs.text.innerHTML = compile(tokens, false);
+				this.$refs.text.innerHTML = compile(tokens, false);
 
-				Array.from(this.refs.text.children).forEach(e => {
+				Array.from(this.$refs.text.children).forEach(e => {
 					if (e.tagName == 'MK-URL') riot.mount(e);
 				});
 			}
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 0616a95f9..115b22c86 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -437,10 +437,10 @@
 		this.refresh = post => {
 			this.set(post);
 			this.update();
-			if (this.refs.reactionsViewer) this.refs.reactionsViewer.update({
+			if (this.$refs.reactionsViewer) this.$refs.reactionsViewer.update({
 				post
 			});
-			if (this.refs.pollViewer) this.refs.pollViewer.init(post);
+			if (this.$refs.pollViewer) this.$refs.pollViewer.init(post);
 		};
 
 		this.onStreamPostUpdated = data => {
@@ -484,9 +484,9 @@
 			if (this.p.text) {
 				const tokens = this.p.ast;
 
-				this.refs.text.innerHTML = this.refs.text.innerHTML.replace('<p class="dummy"></p>', compile(tokens));
+				this.$refs.text.innerHTML = this.$refs.text.innerHTML.replace('<p class="dummy"></p>', compile(tokens));
 
-				Array.from(this.refs.text.children).forEach(e => {
+				Array.from(this.$refs.text.children).forEach(e => {
 					if (e.tagName == 'MK-URL') riot.mount(e);
 				});
 
@@ -494,7 +494,7 @@
 				tokens
 				.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
 				.map(t => {
-					riot.mount(this.refs.text.appendChild(document.createElement('mk-url-preview')), {
+					riot.mount(this.$refs.text.appendChild(document.createElement('mk-url-preview')), {
 						url: t.url
 					});
 				});
@@ -521,14 +521,14 @@
 
 		this.react = () => {
 			riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), {
-				source: this.refs.reactButton,
+				source: this.$refs.reactButton,
 				post: this.p
 			});
 		};
 
 		this.menu = () => {
 			riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
-				source: this.refs.menuButton,
+				source: this.$refs.menuButton,
 				post: this.p
 			});
 		};
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index 3e7b5c2ec..777624d7b 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -180,7 +180,7 @@
 
 		this.onsubmit = e => {
 			e.preventDefault();
-			this.page('/search?q=' + encodeURIComponent(this.refs.q.value));
+			this.page('/search?q=' + encodeURIComponent(this.$refs.q.value));
 		};
 	</script>
 </mk-ui-header-search>
diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/tags/user-timeline.tag
index 19ee2f328..0bfad05c2 100644
--- a/src/web/app/desktop/tags/user-timeline.tag
+++ b/src/web/app/desktop/tags/user-timeline.tag
@@ -88,7 +88,7 @@
 		this.onDocumentKeydown = e => {
 			if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') {
 				if (e.which == 84) { // [t]
-					this.refs.timeline.focus();
+					this.$refs.timeline.focus();
 				}
 			}
 		};
@@ -103,25 +103,25 @@
 					isLoading: false,
 					isEmpty: posts.length == 0
 				});
-				this.refs.timeline.setPosts(posts);
+				this.$refs.timeline.setPosts(posts);
 				if (cb) cb();
 			});
 		};
 
 		this.more = () => {
-			if (this.moreLoading || this.isLoading || this.refs.timeline.posts.length == 0) return;
+			if (this.moreLoading || this.isLoading || this.$refs.timeline.posts.length == 0) return;
 			this.update({
 				moreLoading: true
 			});
 			this.api('users/posts', {
 				user_id: this.user.id,
 				with_replies: this.mode == 'with-replies',
-				until_id: this.refs.timeline.tail().id
+				until_id: this.$refs.timeline.tail().id
 			}).then(posts => {
 				this.update({
 					moreLoading: false
 				});
-				this.refs.timeline.prependPosts(posts);
+				this.$refs.timeline.prependPosts(posts);
 			});
 		};
 
diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/tags/user.tag
index 5dc4175cf..8eca3caaa 100644
--- a/src/web/app/desktop/tags/user.tag
+++ b/src/web/app/desktop/tags/user.tag
@@ -206,10 +206,10 @@
 
 			const z = 1.25; // 奥行き(小さいほど奥)
 			const pos = -(top / z);
-			this.refs.banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
+			this.$refs.banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
 
 			const blur = top / 32
-			if (blur <= 10) this.refs.banner.style.filter = `blur(${blur}px)`;
+			if (blur <= 10) this.$refs.banner.style.filter = `blur(${blur}px)`;
 		};
 
 		this.onUpdateBanner = () => {
@@ -715,12 +715,12 @@
 		this.user = this.opts.user;
 
 		this.on('mount', () => {
-			this.refs.tl.on('loaded', () => {
+			this.$refs.tl.on('loaded', () => {
 				this.trigger('loaded');
 			});
 
-			this.scrollFollowerLeft = new ScrollFollower(this.refs.left, this.parent.root.getBoundingClientRect().top);
-			this.scrollFollowerRight = new ScrollFollower(this.refs.right, this.parent.root.getBoundingClientRect().top);
+			this.scrollFollowerLeft = new ScrollFollower(this.$refs.left, this.parent.root.getBoundingClientRect().top);
+			this.scrollFollowerRight = new ScrollFollower(this.$refs.right, this.parent.root.getBoundingClientRect().top);
 		});
 
 		this.on('unmount', () => {
@@ -729,7 +729,7 @@
 		});
 
 		this.warp = date => {
-			this.refs.tl.warp(date);
+			this.$refs.tl.warp(date);
 		};
 	</script>
 </mk-user-home>
diff --git a/src/web/app/desktop/tags/window.tag b/src/web/app/desktop/tags/window.tag
index ebc7382d5..31830d907 100644
--- a/src/web/app/desktop/tags/window.tag
+++ b/src/web/app/desktop/tags/window.tag
@@ -199,13 +199,13 @@
 		this.canResize = !this.isFlexible;
 
 		this.on('mount', () => {
-			this.refs.main.style.width = this.opts.width || '530px';
-			this.refs.main.style.height = this.opts.height || 'auto';
+			this.$refs.main.style.width = this.opts.width || '530px';
+			this.$refs.main.style.height = this.opts.height || 'auto';
 
-			this.refs.main.style.top = '15%';
-			this.refs.main.style.left = (window.innerWidth / 2) - (this.refs.main.offsetWidth / 2) + 'px';
+			this.$refs.main.style.top = '15%';
+			this.$refs.main.style.left = (window.innerWidth / 2) - (this.$refs.main.offsetWidth / 2) + 'px';
 
-			this.refs.header.addEventListener('contextmenu', e => {
+			this.$refs.header.addEventListener('contextmenu', e => {
 				e.preventDefault();
 			});
 
@@ -219,15 +219,15 @@
 		});
 
 		this.onBrowserResize = () => {
-			const position = this.refs.main.getBoundingClientRect();
+			const position = this.$refs.main.getBoundingClientRect();
 			const browserWidth = window.innerWidth;
 			const browserHeight = window.innerHeight;
-			const windowWidth = this.refs.main.offsetWidth;
-			const windowHeight = this.refs.main.offsetHeight;
-			if (position.left < 0) this.refs.main.style.left = 0;
-			if (position.top < 0) this.refs.main.style.top = 0;
-			if (position.left + windowWidth > browserWidth) this.refs.main.style.left = browserWidth - windowWidth + 'px';
-			if (position.top + windowHeight > browserHeight) this.refs.main.style.top = browserHeight - windowHeight + 'px';
+			const windowWidth = this.$refs.main.offsetWidth;
+			const windowHeight = this.$refs.main.offsetHeight;
+			if (position.left < 0) this.$refs.main.style.left = 0;
+			if (position.top < 0) this.$refs.main.style.top = 0;
+			if (position.left + windowWidth > browserWidth) this.$refs.main.style.left = browserWidth - windowWidth + 'px';
+			if (position.top + windowHeight > browserHeight) this.$refs.main.style.top = browserHeight - windowHeight + 'px';
 		};
 
 		this.open = () => {
@@ -236,25 +236,25 @@
 			this.top();
 
 			if (this.isModal) {
-				this.refs.bg.style.pointerEvents = 'auto';
+				this.$refs.bg.style.pointerEvents = 'auto';
 				anime({
-					targets: this.refs.bg,
+					targets: this.$refs.bg,
 					opacity: 1,
 					duration: 100,
 					easing: 'linear'
 				});
 			}
 
-			this.refs.main.style.pointerEvents = 'auto';
+			this.$refs.main.style.pointerEvents = 'auto';
 			anime({
-				targets: this.refs.main,
+				targets: this.$refs.main,
 				opacity: 1,
 				scale: [1.1, 1],
 				duration: 200,
 				easing: 'easeOutQuad'
 			});
 
-			//this.refs.main.focus();
+			//this.$refs.main.focus();
 
 			setTimeout(() => {
 				this.trigger('opened');
@@ -262,10 +262,10 @@
 		};
 
 		this.popout = () => {
-			const position = this.refs.main.getBoundingClientRect();
+			const position = this.$refs.main.getBoundingClientRect();
 
-			const width = parseInt(getComputedStyle(this.refs.main, '').width, 10);
-			const height = parseInt(getComputedStyle(this.refs.main, '').height, 10);
+			const width = parseInt(getComputedStyle(this.$refs.main, '').width, 10);
+			const height = parseInt(getComputedStyle(this.$refs.main, '').height, 10);
 			const x = window.screenX + position.left;
 			const y = window.screenY + position.top;
 
@@ -281,19 +281,19 @@
 			this.trigger('closing');
 
 			if (this.isModal) {
-				this.refs.bg.style.pointerEvents = 'none';
+				this.$refs.bg.style.pointerEvents = 'none';
 				anime({
-					targets: this.refs.bg,
+					targets: this.$refs.bg,
 					opacity: 0,
 					duration: 300,
 					easing: 'linear'
 				});
 			}
 
-			this.refs.main.style.pointerEvents = 'none';
+			this.$refs.main.style.pointerEvents = 'none';
 
 			anime({
-				targets: this.refs.main,
+				targets: this.$refs.main,
 				opacity: 0,
 				scale: 0.8,
 				duration: 300,
@@ -318,8 +318,8 @@
 			});
 
 			if (z > 0) {
-				this.refs.main.style.zIndex = z + 1;
-				if (this.isModal) this.refs.bg.style.zIndex = z + 1;
+				this.$refs.main.style.zIndex = z + 1;
+				if (this.isModal) this.$refs.bg.style.zIndex = z + 1;
 			}
 		};
 
@@ -340,9 +340,9 @@
 		this.onHeaderMousedown = e => {
 			e.preventDefault();
 
-			if (!contains(this.refs.main, document.activeElement)) this.refs.main.focus();
+			if (!contains(this.$refs.main, document.activeElement)) this.$refs.main.focus();
 
-			const position = this.refs.main.getBoundingClientRect();
+			const position = this.$refs.main.getBoundingClientRect();
 
 			const clickX = e.clientX;
 			const clickY = e.clientY;
@@ -350,8 +350,8 @@
 			const moveBaseY = clickY - position.top;
 			const browserWidth = window.innerWidth;
 			const browserHeight = window.innerHeight;
-			const windowWidth = this.refs.main.offsetWidth;
-			const windowHeight = this.refs.main.offsetHeight;
+			const windowWidth = this.$refs.main.offsetWidth;
+			const windowHeight = this.$refs.main.offsetHeight;
 
 			// 動かした時
 			dragListen(me => {
@@ -370,8 +370,8 @@
 				// 右はみ出し
 				if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
 
-				this.refs.main.style.left = moveLeft + 'px';
-				this.refs.main.style.top = moveTop + 'px';
+				this.$refs.main.style.left = moveLeft + 'px';
+				this.$refs.main.style.top = moveTop + 'px';
 			});
 		};
 
@@ -380,8 +380,8 @@
 			e.preventDefault();
 
 			const base = e.clientY;
-			const height = parseInt(getComputedStyle(this.refs.main, '').height, 10);
-			const top = parseInt(getComputedStyle(this.refs.main, '').top, 10);
+			const height = parseInt(getComputedStyle(this.$refs.main, '').height, 10);
+			const top = parseInt(getComputedStyle(this.$refs.main, '').top, 10);
 
 			// 動かした時
 			dragListen(me => {
@@ -406,8 +406,8 @@
 			e.preventDefault();
 
 			const base = e.clientX;
-			const width = parseInt(getComputedStyle(this.refs.main, '').width, 10);
-			const left = parseInt(getComputedStyle(this.refs.main, '').left, 10);
+			const width = parseInt(getComputedStyle(this.$refs.main, '').width, 10);
+			const left = parseInt(getComputedStyle(this.$refs.main, '').left, 10);
 			const browserWidth = window.innerWidth;
 
 			// 動かした時
@@ -430,8 +430,8 @@
 			e.preventDefault();
 
 			const base = e.clientY;
-			const height = parseInt(getComputedStyle(this.refs.main, '').height, 10);
-			const top = parseInt(getComputedStyle(this.refs.main, '').top, 10);
+			const height = parseInt(getComputedStyle(this.$refs.main, '').height, 10);
+			const top = parseInt(getComputedStyle(this.$refs.main, '').top, 10);
 			const browserHeight = window.innerHeight;
 
 			// 動かした時
@@ -454,8 +454,8 @@
 			e.preventDefault();
 
 			const base = e.clientX;
-			const width = parseInt(getComputedStyle(this.refs.main, '').width, 10);
-			const left = parseInt(getComputedStyle(this.refs.main, '').left, 10);
+			const width = parseInt(getComputedStyle(this.$refs.main, '').width, 10);
+			const left = parseInt(getComputedStyle(this.$refs.main, '').left, 10);
 
 			// 動かした時
 			dragListen(me => {
@@ -501,22 +501,22 @@
 
 		// 高さを適用
 		this.applyTransformHeight = height => {
-			this.refs.main.style.height = height + 'px';
+			this.$refs.main.style.height = height + 'px';
 		};
 
 		// 幅を適用
 		this.applyTransformWidth = width => {
-			this.refs.main.style.width = width + 'px';
+			this.$refs.main.style.width = width + 'px';
 		};
 
 		// Y座標を適用
 		this.applyTransformTop = top => {
-			this.refs.main.style.top = top + 'px';
+			this.$refs.main.style.top = top + 'px';
 		};
 
 		// X座標を適用
 		this.applyTransformLeft = left => {
-			this.refs.main.style.left = left + 'px';
+			this.$refs.main.style.left = left + 'px';
 		};
 
 		function dragListen(fn) {
diff --git a/src/web/app/dev/tags/new-app-form.tag b/src/web/app/dev/tags/new-app-form.tag
index c9518d8de..aba6b1524 100644
--- a/src/web/app/dev/tags/new-app-form.tag
+++ b/src/web/app/dev/tags/new-app-form.tag
@@ -183,7 +183,7 @@
 		this.nidState = null;
 
 		this.onChangeNid = () => {
-			const nid = this.refs.nid.value;
+			const nid = this.$refs.nid.value;
 
 			if (nid == '') {
 				this.update({
@@ -223,13 +223,13 @@
 		};
 
 		this.onsubmit = () => {
-			const name = this.refs.name.value;
-			const nid = this.refs.nid.value;
-			const description = this.refs.description.value;
-			const cb = this.refs.cb.value;
+			const name = this.$refs.name.value;
+			const nid = this.$refs.nid.value;
+			const description = this.$refs.description.value;
+			const cb = this.$refs.cb.value;
 			const permission = [];
 
-			this.refs.permission.querySelectorAll('input').forEach(el => {
+			this.$refs.permission.querySelectorAll('input').forEach(el => {
 				if (el.checked) permission.push(el.value);
 			});
 
diff --git a/src/web/app/mobile/tags/drive-folder-selector.tag b/src/web/app/mobile/tags/drive-folder-selector.tag
index 82e22fed2..37d571d73 100644
--- a/src/web/app/mobile/tags/drive-folder-selector.tag
+++ b/src/web/app/mobile/tags/drive-folder-selector.tag
@@ -62,7 +62,7 @@
 		};
 
 		this.ok = () => {
-			this.trigger('selected', this.refs.browser.folder);
+			this.trigger('selected', this.$refs.browser.folder);
 			this.unmount();
 		};
 	</script>
diff --git a/src/web/app/mobile/tags/drive-selector.tag b/src/web/app/mobile/tags/drive-selector.tag
index 36fed8c32..ab67cc80c 100644
--- a/src/web/app/mobile/tags/drive-selector.tag
+++ b/src/web/app/mobile/tags/drive-selector.tag
@@ -63,13 +63,13 @@
 		this.files = [];
 
 		this.on('mount', () => {
-			this.refs.browser.on('change-selection', files => {
+			this.$refs.browser.on('change-selection', files => {
 				this.update({
 					files: files
 				});
 			});
 
-			this.refs.browser.on('selected', file => {
+			this.$refs.browser.on('selected', file => {
 				this.trigger('selected', file);
 				this.unmount();
 			});
diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index d3ca1aff9..3d0396692 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -209,7 +209,7 @@
 			}
 
 			if (this.opts.isNaked) {
-				this.refs.nav.style.top = `${this.opts.top}px`;
+				this.$refs.nav.style.top = `${this.opts.top}px`;
 			}
 		});
 
@@ -517,7 +517,7 @@
 		};
 
 		this.selectLocalFile = () => {
-			this.refs.file.click();
+			this.$refs.file.click();
 		};
 
 		this.createFolder = () => {
@@ -574,7 +574,7 @@
 		};
 
 		this.changeLocalFile = () => {
-			Array.from(this.refs.file.files).forEach(f => this.refs.uploader.upload(f, this.folder));
+			Array.from(this.$refs.file.files).forEach(f => this.$refs.uploader.upload(f, this.folder));
 		};
 	</script>
 </mk-drive>
diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag
index 2d9338fd3..82fbb6609 100644
--- a/src/web/app/mobile/tags/drive/file-viewer.tag
+++ b/src/web/app/mobile/tags/drive/file-viewer.tag
@@ -243,7 +243,7 @@
 
 		this.onImageLoaded = () => {
 			const self = this;
-			EXIF.getData(this.refs.img, function() {
+			EXIF.getData(this.$refs.img, function() {
 				const allMetaData = EXIF.getAllTags(this);
 				self.update({
 					exif: allMetaData
diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag
index 397d2b398..aa3818007 100644
--- a/src/web/app/mobile/tags/home-timeline.tag
+++ b/src/web/app/mobile/tags/home-timeline.tag
@@ -28,7 +28,7 @@
 
 		this.fetch = () => {
 			this.api('posts/timeline').then(posts => {
-				this.refs.timeline.setPosts(posts);
+				this.$refs.timeline.setPosts(posts);
 			});
 		};
 
@@ -47,7 +47,7 @@
 
 		this.more = () => {
 			return this.api('posts/timeline', {
-				until_id: this.refs.timeline.tail().id
+				until_id: this.$refs.timeline.tail().id
 			});
 		};
 
@@ -55,7 +55,7 @@
 			this.update({
 				isEmpty: false
 			});
-			this.refs.timeline.addPost(post);
+			this.$refs.timeline.addPost(post);
 		};
 
 		this.onStreamFollow = () => {
diff --git a/src/web/app/mobile/tags/home.tag b/src/web/app/mobile/tags/home.tag
index d92e3ae4e..2c07c286d 100644
--- a/src/web/app/mobile/tags/home.tag
+++ b/src/web/app/mobile/tags/home.tag
@@ -15,7 +15,7 @@
 	</style>
 	<script>
 		this.on('mount', () => {
-			this.refs.tl.on('loaded', () => {
+			this.$refs.tl.on('loaded', () => {
 				this.trigger('loaded');
 			});
 		});
diff --git a/src/web/app/mobile/tags/page/drive.tag b/src/web/app/mobile/tags/page/drive.tag
index 0033ffe65..b5ed3385e 100644
--- a/src/web/app/mobile/tags/page/drive.tag
+++ b/src/web/app/mobile/tags/page/drive.tag
@@ -15,22 +15,22 @@
 			ui.trigger('title', '%fa:cloud%%i18n:mobile.tags.mk-drive-page.drive%');
 
 			ui.trigger('func', () => {
-				this.refs.ui.refs.browser.openContextMenu();
+				this.$refs.ui.refs.browser.openContextMenu();
 			}, '%fa:ellipsis-h%');
 
-			this.refs.ui.refs.browser.on('begin-fetch', () => {
+			this.$refs.ui.refs.browser.on('begin-fetch', () => {
 				Progress.start();
 			});
 
-			this.refs.ui.refs.browser.on('fetched-mid', () => {
+			this.$refs.ui.refs.browser.on('fetched-mid', () => {
 				Progress.set(0.5);
 			});
 
-			this.refs.ui.refs.browser.on('fetched', () => {
+			this.$refs.ui.refs.browser.on('fetched', () => {
 				Progress.done();
 			});
 
-			this.refs.ui.refs.browser.on('move-root', () => {
+			this.$refs.ui.refs.browser.on('move-root', () => {
 				const title = 'Misskey Drive';
 
 				// Rewrite URL
@@ -40,7 +40,7 @@
 				ui.trigger('title', '%fa:cloud%%i18n:mobile.tags.mk-drive-page.drive%');
 			});
 
-			this.refs.ui.refs.browser.on('open-folder', (folder, silent) => {
+			this.$refs.ui.refs.browser.on('open-folder', (folder, silent) => {
 				const title = folder.name + ' | Misskey Drive';
 
 				if (!silent) {
@@ -53,7 +53,7 @@
 				ui.trigger('title', '%fa:R folder-open%' + folder.name);
 			});
 
-			this.refs.ui.refs.browser.on('open-file', (file, silent) => {
+			this.$refs.ui.refs.browser.on('open-file', (file, silent) => {
 				const title = file.name + ' | Misskey Drive';
 
 				if (!silent) {
diff --git a/src/web/app/mobile/tags/page/home.tag b/src/web/app/mobile/tags/page/home.tag
index 99cc6b29b..0c3121a21 100644
--- a/src/web/app/mobile/tags/page/home.tag
+++ b/src/web/app/mobile/tags/page/home.tag
@@ -34,7 +34,7 @@
 			this.connection.on('post', this.onStreamPost);
 			document.addEventListener('visibilitychange', this.onVisibilitychange, false);
 
-			this.refs.ui.refs.home.on('loaded', () => {
+			this.$refs.ui.refs.home.on('loaded', () => {
 				Progress.done();
 			});
 		});
diff --git a/src/web/app/mobile/tags/page/messaging.tag b/src/web/app/mobile/tags/page/messaging.tag
index 29e98ce09..76d610377 100644
--- a/src/web/app/mobile/tags/page/messaging.tag
+++ b/src/web/app/mobile/tags/page/messaging.tag
@@ -15,7 +15,7 @@
 			document.title = 'Misskey | %i18n:mobile.tags.mk-messaging-page.message%';
 			ui.trigger('title', '%fa:R comments%%i18n:mobile.tags.mk-messaging-page.message%');
 
-			this.refs.ui.refs.index.on('navigate-user', user => {
+			this.$refs.ui.refs.index.on('navigate-user', user => {
 				this.page('/i/messaging/' + user.username);
 			});
 		});
diff --git a/src/web/app/mobile/tags/page/notifications.tag b/src/web/app/mobile/tags/page/notifications.tag
index 1db9c5d66..596467d47 100644
--- a/src/web/app/mobile/tags/page/notifications.tag
+++ b/src/web/app/mobile/tags/page/notifications.tag
@@ -23,7 +23,7 @@
 
 			Progress.start();
 
-			this.refs.ui.refs.notifications.on('fetched', () => {
+			this.$refs.ui.refs.notifications.on('fetched', () => {
 				Progress.done();
 			});
 		});
diff --git a/src/web/app/mobile/tags/page/search.tag b/src/web/app/mobile/tags/page/search.tag
index 5c39d97e5..51c8cce8b 100644
--- a/src/web/app/mobile/tags/page/search.tag
+++ b/src/web/app/mobile/tags/page/search.tag
@@ -18,7 +18,7 @@
 
 			Progress.start();
 
-			this.refs.ui.refs.search.on('loaded', () => {
+			this.$refs.ui.refs.search.on('loaded', () => {
 				Progress.done();
 			});
 		});
diff --git a/src/web/app/mobile/tags/page/selectdrive.tag b/src/web/app/mobile/tags/page/selectdrive.tag
index 42a624a7a..172a161ec 100644
--- a/src/web/app/mobile/tags/page/selectdrive.tag
+++ b/src/web/app/mobile/tags/page/selectdrive.tag
@@ -59,12 +59,12 @@
 		this.on('mount', () => {
 			document.documentElement.style.background = '#fff';
 
-			this.refs.browser.on('selected', file => {
+			this.$refs.browser.on('selected', file => {
 				this.files = [file];
 				this.ok();
 			});
 
-			this.refs.browser.on('change-selection', files => {
+			this.$refs.browser.on('change-selection', files => {
 				this.update({
 					files: files
 				});
@@ -72,7 +72,7 @@
 		});
 
 		this.upload = () => {
-			this.refs.browser.selectLocalFile();
+			this.$refs.browser.selectLocalFile();
 		};
 
 		this.close = () => {
diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
index cf62c3eb5..5d6c47794 100644
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -231,10 +231,10 @@
 			});
 
 			this.api('i/update', {
-				name: this.refs.name.value,
-				location: this.refs.location.value || null,
-				description: this.refs.description.value || null,
-				birthday: this.refs.birthday.value || null
+				name: this.$refs.name.value,
+				location: this.$refs.location.value || null,
+				description: this.$refs.description.value || null,
+				birthday: this.$refs.birthday.value || null
 			}).then(() => {
 				this.update({
 					saving: false
diff --git a/src/web/app/mobile/tags/page/user-followers.tag b/src/web/app/mobile/tags/page/user-followers.tag
index cffb2b58c..a5e63613c 100644
--- a/src/web/app/mobile/tags/page/user-followers.tag
+++ b/src/web/app/mobile/tags/page/user-followers.tag
@@ -31,7 +31,7 @@
 				ui.trigger('title', '<img src="' + user.avatar_url + '?thumbnail&size=64">' +  '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name));
 				document.documentElement.style.background = '#313a42';
 
-				this.refs.ui.refs.list.on('loaded', () => {
+				this.$refs.ui.refs.list.on('loaded', () => {
 					Progress.done();
 				});
 			});
diff --git a/src/web/app/mobile/tags/page/user-following.tag b/src/web/app/mobile/tags/page/user-following.tag
index 369cb4642..b4ed10783 100644
--- a/src/web/app/mobile/tags/page/user-following.tag
+++ b/src/web/app/mobile/tags/page/user-following.tag
@@ -31,7 +31,7 @@
 				ui.trigger('title', '<img src="' + user.avatar_url + '?thumbnail&size=64">' + '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', user.name));
 				document.documentElement.style.background = '#313a42';
 
-				this.refs.ui.refs.list.on('loaded', () => {
+				this.$refs.ui.refs.list.on('loaded', () => {
 					Progress.done();
 				});
 			});
diff --git a/src/web/app/mobile/tags/page/user.tag b/src/web/app/mobile/tags/page/user.tag
index 78ca534eb..8eec733fc 100644
--- a/src/web/app/mobile/tags/page/user.tag
+++ b/src/web/app/mobile/tags/page/user.tag
@@ -16,7 +16,7 @@
 			document.documentElement.style.background = '#313a42';
 			Progress.start();
 
-			this.refs.ui.refs.user.on('loaded', user => {
+			this.$refs.ui.refs.user.on('loaded', user => {
 				Progress.done();
 				document.title = user.name + ' | Misskey';
 				// TODO: ユーザー名をエスケープ
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index 131ea3aa3..be377d77f 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -273,9 +273,9 @@
 			if (this.p.text) {
 				const tokens = this.p.ast;
 
-				this.refs.text.innerHTML = compile(tokens);
+				this.$refs.text.innerHTML = compile(tokens);
 
-				Array.from(this.refs.text.children).forEach(e => {
+				Array.from(this.$refs.text.children).forEach(e => {
 					if (e.tagName == 'MK-URL') riot.mount(e);
 				});
 
@@ -283,7 +283,7 @@
 				tokens
 				.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
 				.map(t => {
-					riot.mount(this.refs.text.appendChild(document.createElement('mk-url-preview')), {
+					riot.mount(this.$refs.text.appendChild(document.createElement('mk-url-preview')), {
 						url: t.url
 					});
 				});
@@ -319,7 +319,7 @@
 
 		this.react = () => {
 			riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), {
-				source: this.refs.reactButton,
+				source: this.$refs.reactButton,
 				post: this.p,
 				compact: true
 			});
@@ -327,7 +327,7 @@
 
 		this.menu = () => {
 			riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
-				source: this.refs.menuButton,
+				source: this.$refs.menuButton,
 				post: this.p,
 				compact: true
 			});
diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
index f0aa102d6..442919100 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -156,17 +156,17 @@
 		this.poll = false;
 
 		this.on('mount', () => {
-			this.refs.uploader.on('uploaded', file => {
+			this.$refs.uploader.on('uploaded', file => {
 				this.addFile(file);
 			});
 
-			this.refs.uploader.on('change-uploads', uploads => {
+			this.$refs.uploader.on('change-uploads', uploads => {
 				this.trigger('change-uploading-files', uploads);
 			});
 
-			this.refs.text.focus();
+			this.$refs.text.focus();
 
-			new Sortable(this.refs.attaches, {
+			new Sortable(this.$refs.attaches, {
 				animation: 150
 			});
 		});
@@ -184,7 +184,7 @@
 		};
 
 		this.selectFile = () => {
-			this.refs.file.click();
+			this.$refs.file.click();
 		};
 
 		this.selectFileFromDrive = () => {
@@ -197,11 +197,11 @@
 		};
 
 		this.changeFile = () => {
-			Array.from(this.refs.file.files).forEach(this.upload);
+			Array.from(this.$refs.file.files).forEach(this.upload);
 		};
 
 		this.upload = file => {
-			this.refs.uploader.upload(file);
+			this.$refs.uploader.upload(file);
 		};
 
 		this.addFile = file => {
@@ -241,7 +241,7 @@
 			const files = [];
 
 			if (this.files.length > 0) {
-				Array.from(this.refs.attaches.children).forEach(el => {
+				Array.from(this.$refs.attaches.children).forEach(el => {
 					const id = el.getAttribute('data-id');
 					const file = this.files.find(f => f.id == id);
 					files.push(file);
@@ -249,10 +249,10 @@
 			}
 
 			this.api('posts/create', {
-				text: this.refs.text.value == '' ? undefined : this.refs.text.value,
+				text: this.$refs.text.value == '' ? undefined : this.$refs.text.value,
 				media_ids: this.files.length > 0 ? files.map(f => f.id) : undefined,
 				reply_id: opts.reply ? opts.reply.id : undefined,
-				poll: this.poll ? this.refs.poll.get() : undefined
+				poll: this.poll ? this.$refs.poll.get() : undefined
 			}).then(data => {
 				this.trigger('post');
 				this.unmount();
@@ -269,7 +269,7 @@
 		};
 
 		this.kao = () => {
-			this.refs.text.value += getKao();
+			this.$refs.text.value += getKao();
 		};
 	</script>
 </mk-post-form>
diff --git a/src/web/app/mobile/tags/search.tag b/src/web/app/mobile/tags/search.tag
index 2d299e0a7..15a861d7a 100644
--- a/src/web/app/mobile/tags/search.tag
+++ b/src/web/app/mobile/tags/search.tag
@@ -8,7 +8,7 @@
 		this.query = this.opts.query;
 
 		this.on('mount', () => {
-			this.refs.posts.on('loaded', () => {
+			this.$refs.posts.on('loaded', () => {
 				this.trigger('loaded');
 			});
 		});
diff --git a/src/web/app/mobile/tags/sub-post-content.tag b/src/web/app/mobile/tags/sub-post-content.tag
index adeb84dea..7192cd013 100644
--- a/src/web/app/mobile/tags/sub-post-content.tag
+++ b/src/web/app/mobile/tags/sub-post-content.tag
@@ -35,9 +35,9 @@
 		this.on('mount', () => {
 			if (this.post.text) {
 				const tokens = this.post.ast;
-				this.refs.text.innerHTML = compile(tokens, false);
+				this.$refs.text.innerHTML = compile(tokens, false);
 
-				Array.from(this.refs.text.children).forEach(e => {
+				Array.from(this.$refs.text.children).forEach(e => {
 					if (e.tagName == 'MK-URL') riot.mount(e);
 				});
 			}
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 400fa5d85..66f58ff0a 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -482,10 +482,10 @@
 		this.refresh = post => {
 			this.set(post);
 			this.update();
-			if (this.refs.reactionsViewer) this.refs.reactionsViewer.update({
+			if (this.$refs.reactionsViewer) this.$refs.reactionsViewer.update({
 				post
 			});
-			if (this.refs.pollViewer) this.refs.pollViewer.init(post);
+			if (this.$refs.pollViewer) this.$refs.pollViewer.init(post);
 		};
 
 		this.onStreamPostUpdated = data => {
@@ -529,9 +529,9 @@
 			if (this.p.text) {
 				const tokens = this.p.ast;
 
-				this.refs.text.innerHTML = this.refs.text.innerHTML.replace('<p class="dummy"></p>', compile(tokens));
+				this.$refs.text.innerHTML = this.$refs.text.innerHTML.replace('<p class="dummy"></p>', compile(tokens));
 
-				Array.from(this.refs.text.children).forEach(e => {
+				Array.from(this.$refs.text.children).forEach(e => {
 					if (e.tagName == 'MK-URL') riot.mount(e);
 				});
 
@@ -539,7 +539,7 @@
 				tokens
 				.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
 				.map(t => {
-					riot.mount(this.refs.text.appendChild(document.createElement('mk-url-preview')), {
+					riot.mount(this.$refs.text.appendChild(document.createElement('mk-url-preview')), {
 						url: t.url
 					});
 				});
@@ -569,7 +569,7 @@
 
 		this.react = () => {
 			riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), {
-				source: this.refs.reactButton,
+				source: this.$refs.reactButton,
 				post: this.p,
 				compact: true
 			});
@@ -577,7 +577,7 @@
 
 		this.menu = () => {
 			riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
-				source: this.refs.menuButton,
+				source: this.$refs.menuButton,
 				post: this.p,
 				compact: true
 			});
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index b03534f92..c5dc4b2e4 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -30,7 +30,7 @@
 
 		this.toggleDrawer = () => {
 			this.isDrawerOpening = !this.isDrawerOpening;
-			this.refs.nav.root.style.display = this.isDrawerOpening ? 'block' : 'none';
+			this.$refs.nav.root.style.display = this.isDrawerOpening ? 'block' : 'none';
 		};
 
 		this.onStreamNotification = notification => {
@@ -209,7 +209,7 @@
 		};
 
 		this.setTitle = title => {
-			this.refs.title.innerHTML = title;
+			this.$refs.title.innerHTML = title;
 		};
 
 		this.setFunc = (fn, icon) => {
diff --git a/src/web/app/mobile/tags/user-followers.tag b/src/web/app/mobile/tags/user-followers.tag
index b710e376c..c4cdedba8 100644
--- a/src/web/app/mobile/tags/user-followers.tag
+++ b/src/web/app/mobile/tags/user-followers.tag
@@ -20,7 +20,7 @@
 		};
 
 		this.on('mount', () => {
-			this.refs.list.on('loaded', () => {
+			this.$refs.list.on('loaded', () => {
 				this.trigger('loaded');
 			});
 		});
diff --git a/src/web/app/mobile/tags/user-following.tag b/src/web/app/mobile/tags/user-following.tag
index 62ca09181..3a6a54dd7 100644
--- a/src/web/app/mobile/tags/user-following.tag
+++ b/src/web/app/mobile/tags/user-following.tag
@@ -20,7 +20,7 @@
 		};
 
 		this.on('mount', () => {
-			this.refs.list.on('loaded', () => {
+			this.$refs.list.on('loaded', () => {
 				this.trigger('loaded');
 			});
 		});
diff --git a/src/web/app/mobile/tags/user-timeline.tag b/src/web/app/mobile/tags/user-timeline.tag
index 86ead5971..65203fec4 100644
--- a/src/web/app/mobile/tags/user-timeline.tag
+++ b/src/web/app/mobile/tags/user-timeline.tag
@@ -26,7 +26,7 @@
 			return this.api('users/posts', {
 				user_id: this.user.id,
 				with_media: this.withMedia,
-				until_id: this.refs.timeline.tail().id
+				until_id: this.$refs.timeline.tail().id
 			});
 		};
 	</script>
diff --git a/src/web/app/status/tags/index.tag b/src/web/app/status/tags/index.tag
index dcadc6617..198aa89e3 100644
--- a/src/web/app/status/tags/index.tag
+++ b/src/web/app/status/tags/index.tag
@@ -93,7 +93,7 @@
 		});
 
 		this.onStats = stats => {
-			this.refs.chart.addData(1 - stats.cpu_usage);
+			this.$refs.chart.addData(1 - stats.cpu_usage);
 
 			const percentage = (stats.cpu_usage * 100).toFixed(0);
 
@@ -124,7 +124,7 @@
 
 		this.onStats = stats => {
 			stats.mem.used = stats.mem.total - stats.mem.free;
-			this.refs.chart.addData(1 - (stats.mem.used / stats.mem.total));
+			this.$refs.chart.addData(1 - (stats.mem.used / stats.mem.total));
 
 			const percentage = (stats.mem.used / stats.mem.total * 100).toFixed(0);
 

From 1a6a72591fc57a71e675062d8906b9c4095dbb33 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 7 Feb 2018 18:21:37 +0900
Subject: [PATCH 006/286] wip

---
 src/web/app/common/tags/post-menu.tag         |  6 +--
 src/web/app/common/tags/reaction-picker.vue   | 51 +++++++------------
 src/web/app/desktop/tags/contextmenu.tag      |  2 +-
 .../app/desktop/tags/detailed-post-window.tag |  2 +-
 src/web/app/desktop/tags/dialog.tag           |  2 +-
 src/web/app/desktop/tags/donation.tag         |  2 +-
 .../desktop/tags/drive/base-contextmenu.tag   |  2 +-
 .../app/desktop/tags/drive/browser-window.tag |  2 +-
 .../desktop/tags/drive/file-contextmenu.tag   |  2 +-
 .../desktop/tags/drive/folder-contextmenu.tag |  2 +-
 .../app/desktop/tags/following-setuper.tag    |  2 +-
 src/web/app/desktop/tags/images.tag           |  2 +-
 src/web/app/desktop/tags/input-dialog.tag     |  2 +-
 .../desktop/tags/messaging/room-window.tag    |  2 +-
 src/web/app/desktop/tags/messaging/window.tag |  2 +-
 src/web/app/desktop/tags/post-form-window.tag |  2 +-
 src/web/app/desktop/tags/progress-dialog.tag  |  2 +-
 .../app/desktop/tags/repost-form-window.tag   |  2 +-
 .../tags/select-file-from-drive-window.tag    |  2 +-
 .../tags/select-folder-from-drive-window.tag  |  2 +-
 .../desktop/tags/set-avatar-suggestion.tag    |  2 +-
 .../desktop/tags/set-banner-suggestion.tag    |  2 +-
 src/web/app/desktop/tags/settings-window.tag  |  2 +-
 src/web/app/desktop/tags/ui.tag               |  2 +-
 src/web/app/desktop/tags/user-preview.tag     |  2 +-
 .../app/mobile/tags/drive-folder-selector.tag |  4 +-
 src/web/app/mobile/tags/drive-selector.tag    |  6 +--
 src/web/app/mobile/tags/init-following.tag    |  2 +-
 src/web/app/mobile/tags/notify.tag            |  2 +-
 src/web/app/mobile/tags/post-form.tag         |  4 +-
 30 files changed, 54 insertions(+), 67 deletions(-)

diff --git a/src/web/app/common/tags/post-menu.tag b/src/web/app/common/tags/post-menu.tag
index 92b2801f5..2ca8c9602 100644
--- a/src/web/app/common/tags/post-menu.tag
+++ b/src/web/app/common/tags/post-menu.tag
@@ -119,7 +119,7 @@
 				post_id: this.post.id
 			}).then(() => {
 				if (this.opts.cb) this.opts.cb('pinned', '%i18n:common.tags.mk-post-menu.pinned%');
-				this.unmount();
+				this.$destroy();
 			});
 		};
 
@@ -130,7 +130,7 @@
 				category: category
 			}).then(() => {
 				if (this.opts.cb) this.opts.cb('categorized', '%i18n:common.tags.mk-post-menu.categorized%');
-				this.unmount();
+				this.$destroy();
 			});
 		};
 
@@ -150,7 +150,7 @@
 				scale: 0.5,
 				duration: 200,
 				easing: 'easeInBack',
-				complete: () => this.unmount()
+				complete: () => this.$destroy()
 			});
 		};
 	</script>
diff --git a/src/web/app/common/tags/reaction-picker.vue b/src/web/app/common/tags/reaction-picker.vue
index 415737208..496144d88 100644
--- a/src/web/app/common/tags/reaction-picker.vue
+++ b/src/web/app/common/tags/reaction-picker.vue
@@ -74,41 +74,28 @@
 			},
 			onMouseout: function(e) {
 				this.title = placeholder;
+			},
+			close: function() {
+				this.$refs.backdrop.style.pointerEvents = 'none';
+				anime({
+					targets: this.$refs.backdrop,
+					opacity: 0,
+					duration: 200,
+					easing: 'linear'
+				});
+
+				this.$refs.popover.style.pointerEvents = 'none';
+				anime({
+					targets: this.$refs.popover,
+					opacity: 0,
+					scale: 0.5,
+					duration: 200,
+					easing: 'easeInBack',
+					complete: () => this.$destroy()
+				});
 			}
 		}
 	};
-
-	this.mixin('api');
-
-	this.post = this.opts.post;
-	this.source = this.opts.source;
-
-	this.on('mount', () => {
-	});
-
-	this.react = reaction => {
-
-	};
-
-	this.close = () => {
-		this.$refs.backdrop.style.pointerEvents = 'none';
-		anime({
-			targets: this.$refs.backdrop,
-			opacity: 0,
-			duration: 200,
-			easing: 'linear'
-		});
-
-		this.$refs.popover.style.pointerEvents = 'none';
-		anime({
-			targets: this.$refs.popover,
-			opacity: 0,
-			scale: 0.5,
-			duration: 200,
-			easing: 'easeInBack',
-			complete: () => this.unmount()
-		});
-	};
 </script>
 
 <mk-reaction-picker>
diff --git a/src/web/app/desktop/tags/contextmenu.tag b/src/web/app/desktop/tags/contextmenu.tag
index 2a3b2a772..ade44fce2 100644
--- a/src/web/app/desktop/tags/contextmenu.tag
+++ b/src/web/app/desktop/tags/contextmenu.tag
@@ -132,7 +132,7 @@
 			});
 
 			this.trigger('closed');
-			this.unmount();
+			this.$destroy();
 		};
 	</script>
 </mk-contextmenu>
diff --git a/src/web/app/desktop/tags/detailed-post-window.tag b/src/web/app/desktop/tags/detailed-post-window.tag
index 93df377c4..6d6f23ac3 100644
--- a/src/web/app/desktop/tags/detailed-post-window.tag
+++ b/src/web/app/desktop/tags/detailed-post-window.tag
@@ -69,7 +69,7 @@
 				opacity: 0,
 				duration: 300,
 				easing: 'linear',
-				complete: () => this.unmount()
+				complete: () => this.$destroy()
 			});
 		};
 
diff --git a/src/web/app/desktop/tags/dialog.tag b/src/web/app/desktop/tags/dialog.tag
index 9299e9733..aff855251 100644
--- a/src/web/app/desktop/tags/dialog.tag
+++ b/src/web/app/desktop/tags/dialog.tag
@@ -130,7 +130,7 @@
 				scale: 0.8,
 				duration: 300,
 				easing: [ 0.5, -0.5, 1, 0.5 ],
-				complete: () => this.unmount()
+				complete: () => this.$destroy()
 			});
 		};
 
diff --git a/src/web/app/desktop/tags/donation.tag b/src/web/app/desktop/tags/donation.tag
index b2d18d445..73ee9d003 100644
--- a/src/web/app/desktop/tags/donation.tag
+++ b/src/web/app/desktop/tags/donation.tag
@@ -60,7 +60,7 @@
 				show_donation: false
 			});
 
-			this.unmount();
+			this.$destroy();
 		};
 	</script>
 </mk-donation>
diff --git a/src/web/app/desktop/tags/drive/base-contextmenu.tag b/src/web/app/desktop/tags/drive/base-contextmenu.tag
index eb97ccccc..d2381cc47 100644
--- a/src/web/app/desktop/tags/drive/base-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/base-contextmenu.tag
@@ -18,7 +18,7 @@
 		this.on('mount', () => {
 			this.$refs.ctx.on('closed', () => {
 				this.trigger('closed');
-				this.unmount();
+				this.$destroy();
 			});
 		});
 
diff --git a/src/web/app/desktop/tags/drive/browser-window.tag b/src/web/app/desktop/tags/drive/browser-window.tag
index 01cb4b1af..f49921eb6 100644
--- a/src/web/app/desktop/tags/drive/browser-window.tag
+++ b/src/web/app/desktop/tags/drive/browser-window.tag
@@ -43,7 +43,7 @@
 
 		this.on('mount', () => {
 			this.$refs.window.on('closed', () => {
-				this.unmount();
+				this.$destroy();
 			});
 
 			this.api('drive').then(info => {
diff --git a/src/web/app/desktop/tags/drive/file-contextmenu.tag b/src/web/app/desktop/tags/drive/file-contextmenu.tag
index 25721372b..bb934d35e 100644
--- a/src/web/app/desktop/tags/drive/file-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/file-contextmenu.tag
@@ -50,7 +50,7 @@
 		this.on('mount', () => {
 			this.$refs.ctx.on('closed', () => {
 				this.trigger('closed');
-				this.unmount();
+				this.$destroy();
 			});
 		});
 
diff --git a/src/web/app/desktop/tags/drive/folder-contextmenu.tag b/src/web/app/desktop/tags/drive/folder-contextmenu.tag
index d424482fa..43cad3da5 100644
--- a/src/web/app/desktop/tags/drive/folder-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/folder-contextmenu.tag
@@ -30,7 +30,7 @@
 
 			this.$refs.ctx.on('closed', () => {
 				this.trigger('closed');
-				this.unmount();
+				this.$destroy();
 			});
 		};
 
diff --git a/src/web/app/desktop/tags/following-setuper.tag b/src/web/app/desktop/tags/following-setuper.tag
index 828098629..d8cd32a20 100644
--- a/src/web/app/desktop/tags/following-setuper.tag
+++ b/src/web/app/desktop/tags/following-setuper.tag
@@ -163,7 +163,7 @@
 		};
 
 		this.close = () => {
-			this.unmount();
+			this.$destroy();
 		};
 	</script>
 </mk-following-setuper>
diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
index dcd664e72..8c4234a0f 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -165,7 +165,7 @@
 				opacity: 0,
 				duration: 100,
 				easing: 'linear',
-				complete: () => this.unmount()
+				complete: () => this.$destroy()
 			});
 		};
 	</script>
diff --git a/src/web/app/desktop/tags/input-dialog.tag b/src/web/app/desktop/tags/input-dialog.tag
index bea8c2c22..1eef25db1 100644
--- a/src/web/app/desktop/tags/input-dialog.tag
+++ b/src/web/app/desktop/tags/input-dialog.tag
@@ -142,7 +142,7 @@
 			});
 
 			this.$refs.window.on('closed', () => {
-				this.unmount();
+				this.$destroy();
 			});
 		});
 
diff --git a/src/web/app/desktop/tags/messaging/room-window.tag b/src/web/app/desktop/tags/messaging/room-window.tag
index bae456200..39afbe6dd 100644
--- a/src/web/app/desktop/tags/messaging/room-window.tag
+++ b/src/web/app/desktop/tags/messaging/room-window.tag
@@ -25,7 +25,7 @@
 
 		this.on('mount', () => {
 			this.$refs.window.on('closed', () => {
-				this.unmount();
+				this.$destroy();
 			});
 		});
 	</script>
diff --git a/src/web/app/desktop/tags/messaging/window.tag b/src/web/app/desktop/tags/messaging/window.tag
index afe01c53e..cd756daa0 100644
--- a/src/web/app/desktop/tags/messaging/window.tag
+++ b/src/web/app/desktop/tags/messaging/window.tag
@@ -21,7 +21,7 @@
 	<script>
 		this.on('mount', () => {
 			this.$refs.window.on('closed', () => {
-				this.unmount();
+				this.$destroy();
 			});
 
 			this.$refs.window.refs.index.on('navigate-user', user => {
diff --git a/src/web/app/desktop/tags/post-form-window.tag b/src/web/app/desktop/tags/post-form-window.tag
index de349bada..8955d0679 100644
--- a/src/web/app/desktop/tags/post-form-window.tag
+++ b/src/web/app/desktop/tags/post-form-window.tag
@@ -45,7 +45,7 @@
 			this.$refs.window.refs.form.focus();
 
 			this.$refs.window.on('closed', () => {
-				this.unmount();
+				this.$destroy();
 			});
 
 			this.$refs.window.refs.form.on('post', () => {
diff --git a/src/web/app/desktop/tags/progress-dialog.tag b/src/web/app/desktop/tags/progress-dialog.tag
index 94e7f8af4..ef055c35b 100644
--- a/src/web/app/desktop/tags/progress-dialog.tag
+++ b/src/web/app/desktop/tags/progress-dialog.tag
@@ -79,7 +79,7 @@
 
 		this.on('mount', () => {
 			this.$refs.window.on('closed', () => {
-				this.unmount();
+				this.$destroy();
 			});
 		});
 
diff --git a/src/web/app/desktop/tags/repost-form-window.tag b/src/web/app/desktop/tags/repost-form-window.tag
index 939ff4e38..b501eb076 100644
--- a/src/web/app/desktop/tags/repost-form-window.tag
+++ b/src/web/app/desktop/tags/repost-form-window.tag
@@ -36,7 +36,7 @@
 			document.addEventListener('keydown', this.onDocumentKeydown);
 
 			this.$refs.window.on('closed', () => {
-				this.unmount();
+				this.$destroy();
 			});
 		});
 
diff --git a/src/web/app/desktop/tags/select-file-from-drive-window.tag b/src/web/app/desktop/tags/select-file-from-drive-window.tag
index 6d1e59413..3e0f00c2f 100644
--- a/src/web/app/desktop/tags/select-file-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-file-from-drive-window.tag
@@ -153,7 +153,7 @@
 			});
 
 			this.$refs.window.on('closed', () => {
-				this.unmount();
+				this.$destroy();
 			});
 		});
 
diff --git a/src/web/app/desktop/tags/select-folder-from-drive-window.tag b/src/web/app/desktop/tags/select-folder-from-drive-window.tag
index 7bfe5af35..ad4ae4caf 100644
--- a/src/web/app/desktop/tags/select-folder-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-folder-from-drive-window.tag
@@ -96,7 +96,7 @@
 
 		this.on('mount', () => {
 			this.$refs.window.on('closed', () => {
-				this.unmount();
+				this.$destroy();
 			});
 		});
 
diff --git a/src/web/app/desktop/tags/set-avatar-suggestion.tag b/src/web/app/desktop/tags/set-avatar-suggestion.tag
index faf4cdd8a..82a438fb7 100644
--- a/src/web/app/desktop/tags/set-avatar-suggestion.tag
+++ b/src/web/app/desktop/tags/set-avatar-suggestion.tag
@@ -42,7 +42,7 @@
 		this.close = e => {
 			e.preventDefault();
 			e.stopPropagation();
-			this.unmount();
+			this.$destroy();
 		};
 	</script>
 </mk-set-avatar-suggestion>
diff --git a/src/web/app/desktop/tags/set-banner-suggestion.tag b/src/web/app/desktop/tags/set-banner-suggestion.tag
index cbf0f1b68..c5c5c7019 100644
--- a/src/web/app/desktop/tags/set-banner-suggestion.tag
+++ b/src/web/app/desktop/tags/set-banner-suggestion.tag
@@ -42,7 +42,7 @@
 		this.close = e => {
 			e.preventDefault();
 			e.stopPropagation();
-			this.unmount();
+			this.$destroy();
 		};
 	</script>
 </mk-set-banner-suggestion>
diff --git a/src/web/app/desktop/tags/settings-window.tag b/src/web/app/desktop/tags/settings-window.tag
index e68a44a4f..09566b898 100644
--- a/src/web/app/desktop/tags/settings-window.tag
+++ b/src/web/app/desktop/tags/settings-window.tag
@@ -19,7 +19,7 @@
 	<script>
 		this.on('mount', () => {
 			this.$refs.window.on('closed', () => {
-				this.unmount();
+				this.$destroy();
 			});
 		});
 
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index 777624d7b..4b302a0eb 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -888,7 +888,7 @@
 					translateY: -64,
 					duration: 500,
 					easing: 'easeInElastic',
-					complete: () => this.unmount()
+					complete: () => this.$destroy()
 				});
 			}, 6000);
 		});
diff --git a/src/web/app/desktop/tags/user-preview.tag b/src/web/app/desktop/tags/user-preview.tag
index b836ff1e7..7993895a8 100644
--- a/src/web/app/desktop/tags/user-preview.tag
+++ b/src/web/app/desktop/tags/user-preview.tag
@@ -142,7 +142,7 @@
 				'margin-top': '-8px',
 				duration: 200,
 				easing: 'easeOutQuad',
-				complete: () => this.unmount()
+				complete: () => this.$destroy()
 			});
 		};
 	</script>
diff --git a/src/web/app/mobile/tags/drive-folder-selector.tag b/src/web/app/mobile/tags/drive-folder-selector.tag
index 37d571d73..6a0cb5cea 100644
--- a/src/web/app/mobile/tags/drive-folder-selector.tag
+++ b/src/web/app/mobile/tags/drive-folder-selector.tag
@@ -58,12 +58,12 @@
 	<script>
 		this.cancel = () => {
 			this.trigger('canceled');
-			this.unmount();
+			this.$destroy();
 		};
 
 		this.ok = () => {
 			this.trigger('selected', this.$refs.browser.folder);
-			this.unmount();
+			this.$destroy();
 		};
 	</script>
 </mk-drive-folder-selector>
diff --git a/src/web/app/mobile/tags/drive-selector.tag b/src/web/app/mobile/tags/drive-selector.tag
index ab67cc80c..9e6f6a045 100644
--- a/src/web/app/mobile/tags/drive-selector.tag
+++ b/src/web/app/mobile/tags/drive-selector.tag
@@ -71,18 +71,18 @@
 
 			this.$refs.browser.on('selected', file => {
 				this.trigger('selected', file);
-				this.unmount();
+				this.$destroy();
 			});
 		});
 
 		this.cancel = () => {
 			this.trigger('canceled');
-			this.unmount();
+			this.$destroy();
 		};
 
 		this.ok = () => {
 			this.trigger('selected', this.files);
-			this.unmount();
+			this.$destroy();
 		};
 	</script>
 </mk-drive-selector>
diff --git a/src/web/app/mobile/tags/init-following.tag b/src/web/app/mobile/tags/init-following.tag
index d2d19a887..d7e31b460 100644
--- a/src/web/app/mobile/tags/init-following.tag
+++ b/src/web/app/mobile/tags/init-following.tag
@@ -124,7 +124,7 @@
 		};
 
 		this.close = () => {
-			this.unmount();
+			this.$destroy();
 		};
 	</script>
 </mk-init-following>
diff --git a/src/web/app/mobile/tags/notify.tag b/src/web/app/mobile/tags/notify.tag
index 2dfc2dddb..386166f7f 100644
--- a/src/web/app/mobile/tags/notify.tag
+++ b/src/web/app/mobile/tags/notify.tag
@@ -32,7 +32,7 @@
 					bottom: '-64px',
 					duration: 500,
 					easing: 'easeOutQuad',
-					complete: () => this.unmount()
+					complete: () => this.$destroy()
 				});
 			}, 6000);
 		});
diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
index 442919100..6f0794753 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -255,7 +255,7 @@
 				poll: this.poll ? this.$refs.poll.get() : undefined
 			}).then(data => {
 				this.trigger('post');
-				this.unmount();
+				this.$destroy();
 			}).catch(err => {
 				this.update({
 					wait: false
@@ -265,7 +265,7 @@
 
 		this.cancel = () => {
 			this.trigger('cancel');
-			this.unmount();
+			this.$destroy();
 		};
 
 		this.kao = () => {

From 063193f429f5a8a9843fe1f13696c9d22a261b9e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 7 Feb 2018 18:30:17 +0900
Subject: [PATCH 007/286] wip

---
 src/web/app/auth/tags/form.tag                |   2 +-
 src/web/app/auth/tags/index.tag               |   2 +-
 src/web/app/ch/tags/channel.tag               |   6 +-
 src/web/app/ch/tags/header.tag                |   2 +-
 src/web/app/ch/tags/index.tag                 |   2 +-
 src/web/app/common/tags/activity-table.tag    |   2 +-
 src/web/app/common/tags/authorized-apps.tag   |   2 +-
 src/web/app/common/tags/ellipsis.tag          |   2 +-
 src/web/app/common/tags/error.tag             |   4 +-
 src/web/app/common/tags/file-type-icon.tag    |   2 +-
 src/web/app/common/tags/forkit.tag            |   2 +-
 src/web/app/common/tags/introduction.tag      |   2 +-
 src/web/app/common/tags/messaging/form.tag    |   2 +-
 src/web/app/common/tags/messaging/index.tag   |   2 +-
 src/web/app/common/tags/messaging/message.tag |   2 +-
 src/web/app/common/tags/messaging/room.tag    |   2 +-
 src/web/app/common/tags/nav-links.tag         |   2 +-
 src/web/app/common/tags/number.tag            |   2 +-
 src/web/app/common/tags/poll-editor.tag       |   2 +-
 src/web/app/common/tags/poll.tag              |   2 +-
 src/web/app/common/tags/post-menu.tag         |   2 +-
 src/web/app/common/tags/raw.tag               |   2 +-
 src/web/app/common/tags/reaction-icon.tag     |   2 +-
 src/web/app/common/tags/reaction-picker.vue   | 144 +++++++++---------
 src/web/app/common/tags/reactions-viewer.tag  |   2 +-
 src/web/app/common/tags/signin-history.tag    |   4 +-
 src/web/app/common/tags/signin.tag            |   2 +-
 src/web/app/common/tags/signup.tag            |   2 +-
 src/web/app/common/tags/special-message.tag   |   2 +-
 src/web/app/common/tags/stream-indicator.vue  |   2 +-
 src/web/app/common/tags/twitter-setting.tag   |   2 +-
 src/web/app/common/tags/uploader.tag          |   2 +-
 src/web/app/desktop/tags/analog-clock.tag     |   2 +-
 .../desktop/tags/autocomplete-suggestion.tag  |   2 +-
 .../app/desktop/tags/big-follow-button.tag    |   2 +-
 src/web/app/desktop/tags/contextmenu.tag      |   2 +-
 src/web/app/desktop/tags/crop-window.tag      |   2 +-
 .../app/desktop/tags/detailed-post-window.tag |   2 +-
 src/web/app/desktop/tags/dialog.tag           |   2 +-
 src/web/app/desktop/tags/donation.tag         |   2 +-
 .../app/desktop/tags/drive/browser-window.tag |   2 +-
 src/web/app/desktop/tags/drive/browser.tag    |   2 +-
 src/web/app/desktop/tags/drive/file.tag       |   2 +-
 src/web/app/desktop/tags/drive/folder.tag     |   2 +-
 src/web/app/desktop/tags/drive/nav-folder.tag |   2 +-
 src/web/app/desktop/tags/ellipsis-icon.tag    |   2 +-
 src/web/app/desktop/tags/follow-button.tag    |   2 +-
 .../app/desktop/tags/following-setuper.tag    |   2 +-
 .../desktop/tags/home-widgets/access-log.tag  |   2 +-
 .../desktop/tags/home-widgets/activity.tag    |   2 +-
 .../desktop/tags/home-widgets/broadcast.tag   |   2 +-
 .../desktop/tags/home-widgets/calendar.tag    |   2 +-
 .../app/desktop/tags/home-widgets/channel.tag |   8 +-
 .../desktop/tags/home-widgets/donation.tag    |   2 +-
 .../desktop/tags/home-widgets/mentions.tag    |   2 +-
 .../desktop/tags/home-widgets/messaging.tag   |   2 +-
 src/web/app/desktop/tags/home-widgets/nav.tag |   2 +-
 .../tags/home-widgets/notifications.tag       |   2 +-
 .../tags/home-widgets/photo-stream.tag        |   2 +-
 .../desktop/tags/home-widgets/post-form.tag   |   2 +-
 .../app/desktop/tags/home-widgets/profile.tag |   2 +-
 .../tags/home-widgets/recommended-polls.tag   |   2 +-
 .../desktop/tags/home-widgets/rss-reader.tag  |   2 +-
 .../app/desktop/tags/home-widgets/server.tag  |  16 +-
 .../desktop/tags/home-widgets/slideshow.tag   |   2 +-
 .../desktop/tags/home-widgets/timeline.tag    |   2 +-
 .../desktop/tags/home-widgets/timemachine.tag |   2 +-
 .../app/desktop/tags/home-widgets/tips.tag    |   2 +-
 .../app/desktop/tags/home-widgets/trends.tag  |   2 +-
 .../tags/home-widgets/user-recommendation.tag |   2 +-
 .../app/desktop/tags/home-widgets/version.tag |   2 +-
 src/web/app/desktop/tags/home.tag             |   2 +-
 src/web/app/desktop/tags/images.tag           |   6 +-
 src/web/app/desktop/tags/input-dialog.tag     |   2 +-
 src/web/app/desktop/tags/list-user.tag        |   2 +-
 .../desktop/tags/messaging/room-window.tag    |   2 +-
 src/web/app/desktop/tags/messaging/window.tag |   2 +-
 src/web/app/desktop/tags/notifications.tag    |   2 +-
 src/web/app/desktop/tags/pages/drive.tag      |   2 +-
 src/web/app/desktop/tags/pages/entrance.tag   |   6 +-
 .../app/desktop/tags/pages/home-customize.tag |   2 +-
 src/web/app/desktop/tags/pages/home.tag       |   2 +-
 .../app/desktop/tags/pages/messaging-room.tag |   2 +-
 src/web/app/desktop/tags/pages/not-found.tag  |   2 +-
 src/web/app/desktop/tags/pages/post.tag       |   2 +-
 src/web/app/desktop/tags/pages/search.tag     |   2 +-
 .../app/desktop/tags/pages/selectdrive.tag    |   2 +-
 src/web/app/desktop/tags/pages/user.tag       |   2 +-
 src/web/app/desktop/tags/post-detail-sub.tag  |   2 +-
 src/web/app/desktop/tags/post-detail.tag      |   2 +-
 src/web/app/desktop/tags/post-form-window.tag |   2 +-
 src/web/app/desktop/tags/post-form.tag        |   2 +-
 src/web/app/desktop/tags/post-preview.tag     |   2 +-
 src/web/app/desktop/tags/progress-dialog.tag  |   2 +-
 .../app/desktop/tags/repost-form-window.tag   |   2 +-
 src/web/app/desktop/tags/repost-form.tag      |   2 +-
 src/web/app/desktop/tags/search-posts.tag     |   2 +-
 src/web/app/desktop/tags/search.tag           |   2 +-
 .../tags/select-file-from-drive-window.tag    |   2 +-
 .../tags/select-folder-from-drive-window.tag  |   2 +-
 .../desktop/tags/set-avatar-suggestion.tag    |   2 +-
 .../desktop/tags/set-banner-suggestion.tag    |   2 +-
 src/web/app/desktop/tags/settings-window.tag  |   2 +-
 src/web/app/desktop/tags/settings.tag         |  14 +-
 src/web/app/desktop/tags/sub-post-content.tag |   2 +-
 src/web/app/desktop/tags/timeline.tag         |   6 +-
 src/web/app/desktop/tags/ui.tag               |  18 +--
 .../desktop/tags/user-followers-window.tag    |   2 +-
 src/web/app/desktop/tags/user-followers.tag   |   2 +-
 .../desktop/tags/user-following-window.tag    |   2 +-
 src/web/app/desktop/tags/user-following.tag   |   2 +-
 src/web/app/desktop/tags/user-preview.tag     |   2 +-
 src/web/app/desktop/tags/user-timeline.tag    |   2 +-
 src/web/app/desktop/tags/user.tag             |  18 +--
 src/web/app/desktop/tags/users-list.tag       |   2 +-
 src/web/app/desktop/tags/widgets/activity.tag |   6 +-
 src/web/app/desktop/tags/widgets/calendar.tag |   2 +-
 src/web/app/desktop/tags/window.tag           |   2 +-
 src/web/app/dev/tags/new-app-form.tag         |   2 +-
 src/web/app/dev/tags/pages/app.tag            |   2 +-
 src/web/app/dev/tags/pages/apps.tag           |   2 +-
 src/web/app/dev/tags/pages/index.tag          |   2 +-
 src/web/app/dev/tags/pages/new-app.tag        |   2 +-
 .../app/mobile/tags/drive-folder-selector.tag |   2 +-
 src/web/app/mobile/tags/drive-selector.tag    |   2 +-
 src/web/app/mobile/tags/drive.tag             |   2 +-
 src/web/app/mobile/tags/drive/file-viewer.tag |   2 +-
 src/web/app/mobile/tags/drive/file.tag        |   2 +-
 src/web/app/mobile/tags/drive/folder.tag      |   2 +-
 src/web/app/mobile/tags/follow-button.tag     |   2 +-
 src/web/app/mobile/tags/home-timeline.tag     |   2 +-
 src/web/app/mobile/tags/home.tag              |   2 +-
 src/web/app/mobile/tags/images.tag            |   4 +-
 src/web/app/mobile/tags/init-following.tag    |   2 +-
 .../app/mobile/tags/notification-preview.tag  |   2 +-
 src/web/app/mobile/tags/notification.tag      |   2 +-
 src/web/app/mobile/tags/notifications.tag     |   2 +-
 src/web/app/mobile/tags/notify.tag            |   2 +-
 src/web/app/mobile/tags/page/drive.tag        |   2 +-
 src/web/app/mobile/tags/page/entrance.tag     |   2 +-
 .../app/mobile/tags/page/entrance/signin.tag  |   2 +-
 .../app/mobile/tags/page/entrance/signup.tag  |   2 +-
 src/web/app/mobile/tags/page/home.tag         |   2 +-
 .../app/mobile/tags/page/messaging-room.tag   |   2 +-
 src/web/app/mobile/tags/page/messaging.tag    |   2 +-
 src/web/app/mobile/tags/page/new-post.tag     |   2 +-
 .../app/mobile/tags/page/notifications.tag    |   2 +-
 src/web/app/mobile/tags/page/post.tag         |   2 +-
 src/web/app/mobile/tags/page/search.tag       |   2 +-
 src/web/app/mobile/tags/page/selectdrive.tag  |   2 +-
 src/web/app/mobile/tags/page/settings.tag     |   4 +-
 .../tags/page/settings/authorized-apps.tag    |   2 +-
 .../app/mobile/tags/page/settings/profile.tag |   4 +-
 .../app/mobile/tags/page/settings/signin.tag  |   2 +-
 .../app/mobile/tags/page/settings/twitter.tag |   2 +-
 .../app/mobile/tags/page/user-followers.tag   |   2 +-
 .../app/mobile/tags/page/user-following.tag   |   2 +-
 src/web/app/mobile/tags/page/user.tag         |   2 +-
 src/web/app/mobile/tags/post-detail.tag       |   4 +-
 src/web/app/mobile/tags/post-form.tag         |   2 +-
 src/web/app/mobile/tags/post-preview.tag      |   2 +-
 src/web/app/mobile/tags/search-posts.tag      |   2 +-
 src/web/app/mobile/tags/search.tag            |   2 +-
 src/web/app/mobile/tags/sub-post-content.tag  |   2 +-
 src/web/app/mobile/tags/timeline.tag          |   6 +-
 src/web/app/mobile/tags/ui.tag                |   6 +-
 src/web/app/mobile/tags/user-card.tag         |   2 +-
 src/web/app/mobile/tags/user-followers.tag    |   2 +-
 src/web/app/mobile/tags/user-following.tag    |   2 +-
 src/web/app/mobile/tags/user-preview.tag      |   2 +-
 src/web/app/mobile/tags/user-timeline.tag     |   2 +-
 src/web/app/mobile/tags/user.tag              |  20 +--
 src/web/app/mobile/tags/users-list.tag        |   2 +-
 src/web/app/stats/tags/index.tag              |  10 +-
 src/web/app/status/tags/index.tag             |   8 +-
 175 files changed, 312 insertions(+), 316 deletions(-)

diff --git a/src/web/app/auth/tags/form.tag b/src/web/app/auth/tags/form.tag
index 5bb27c269..8f60aadb5 100644
--- a/src/web/app/auth/tags/form.tag
+++ b/src/web/app/auth/tags/form.tag
@@ -29,7 +29,7 @@
 		<button @click="cancel">キャンセル</button>
 		<button @click="accept">アクセスを許可</button>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/auth/tags/index.tag b/src/web/app/auth/tags/index.tag
index 8d70a4162..e1c0cb82e 100644
--- a/src/web/app/auth/tags/index.tag
+++ b/src/web/app/auth/tags/index.tag
@@ -20,7 +20,7 @@
 		<mk-signin/>
 	</main>
 	<footer><img src="/assets/auth/logo.svg" alt="Misskey"/></footer>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index fec542500..ea0234340 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -33,7 +33,7 @@
 			<small><a href={ _URL_ }>Misskey</a> ver { _VERSION_ } (葵 aoi)</small>
 		</footer>
 	</main>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -181,7 +181,7 @@
 			</virtual>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
@@ -255,7 +255,7 @@
 		<li each={ files }>{ name }</li>
 	</ol>
 	<input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/ch/tags/header.tag b/src/web/app/ch/tags/header.tag
index dec83c9a5..8af6f1c37 100644
--- a/src/web/app/ch/tags/header.tag
+++ b/src/web/app/ch/tags/header.tag
@@ -6,7 +6,7 @@
 		<a if={ !SIGNIN } href={ _URL_ }>ログイン(新規登録)</a>
 		<a if={ SIGNIN } href={ _URL_ + '/' + I.username }>{ I.username }</a>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display flex
 
diff --git a/src/web/app/ch/tags/index.tag b/src/web/app/ch/tags/index.tag
index 489f21148..2fd549368 100644
--- a/src/web/app/ch/tags/index.tag
+++ b/src/web/app/ch/tags/index.tag
@@ -6,7 +6,7 @@
 	<ul if={ channels }>
 		<li each={ channels }><a href={ '/' + this.id }>{ this.title }</a></li>
 	</ul>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/common/tags/activity-table.tag b/src/web/app/common/tags/activity-table.tag
index 1d26d1788..b0a100090 100644
--- a/src/web/app/common/tags/activity-table.tag
+++ b/src/web/app/common/tags/activity-table.tag
@@ -12,7 +12,7 @@
 			stroke-width="0.1"
 			stroke="#f73520"/>
 	</svg>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			max-width 600px
diff --git a/src/web/app/common/tags/authorized-apps.tag b/src/web/app/common/tags/authorized-apps.tag
index 0594032de..324871949 100644
--- a/src/web/app/common/tags/authorized-apps.tag
+++ b/src/web/app/common/tags/authorized-apps.tag
@@ -8,7 +8,7 @@
 			<p>{ app.description }</p>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/common/tags/ellipsis.tag b/src/web/app/common/tags/ellipsis.tag
index 97ef745d0..734454e4a 100644
--- a/src/web/app/common/tags/ellipsis.tag
+++ b/src/web/app/common/tags/ellipsis.tag
@@ -1,5 +1,5 @@
 <mk-ellipsis><span>.</span><span>.</span><span>.</span>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display inline
 
diff --git a/src/web/app/common/tags/error.tag b/src/web/app/common/tags/error.tag
index 07ba61161..0a6535762 100644
--- a/src/web/app/common/tags/error.tag
+++ b/src/web/app/common/tags/error.tag
@@ -11,7 +11,7 @@
 	<button if={ !troubleshooting } @click="troubleshoot">%i18n:common.tags.mk-error.troubleshoot%</button>
 	<mk-troubleshooter if={ troubleshooting }/>
 	<p class="thanks">%i18n:common.tags.mk-error.thanks%</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			width 100%
@@ -108,7 +108,7 @@
 	<p if={ server === false }><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-server%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-server-desc%</p>
 	<p if={ server === true } class="success"><b>%fa:info-circle%%i18n:common.tags.mk-error.troubleshooter.success%</b><br>%i18n:common.tags.mk-error.troubleshooter.success-desc%</p>
 
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			width 100%
diff --git a/src/web/app/common/tags/file-type-icon.tag b/src/web/app/common/tags/file-type-icon.tag
index dba2ae44d..035aec247 100644
--- a/src/web/app/common/tags/file-type-icon.tag
+++ b/src/web/app/common/tags/file-type-icon.tag
@@ -1,6 +1,6 @@
 <mk-file-type-icon>
 	<virtual if={ kind == 'image' }>%fa:file-image%</virtual>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display inline
 	</style>
diff --git a/src/web/app/common/tags/forkit.tag b/src/web/app/common/tags/forkit.tag
index 55d573108..6a8d06e56 100644
--- a/src/web/app/common/tags/forkit.tag
+++ b/src/web/app/common/tags/forkit.tag
@@ -4,7 +4,7 @@
 			<path class="octo-arm" d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor"></path>
 			<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor"></path>
 		</svg></a>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			position absolute
diff --git a/src/web/app/common/tags/introduction.tag b/src/web/app/common/tags/introduction.tag
index 28afc6fa4..c92cff0d1 100644
--- a/src/web/app/common/tags/introduction.tag
+++ b/src/web/app/common/tags/introduction.tag
@@ -5,7 +5,7 @@
 		<p>無料で誰でも利用でき、広告も掲載していません。</p>
 		<p><a href={ _DOCS_URL_ } target="_blank">もっと知りたい方はこちら</a></p>
 	</article>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/common/tags/messaging/form.tag b/src/web/app/common/tags/messaging/form.tag
index 93733e8d7..33b0beb88 100644
--- a/src/web/app/common/tags/messaging/form.tag
+++ b/src/web/app/common/tags/messaging/form.tag
@@ -12,7 +12,7 @@
 		%fa:R folder-open%
 	</button>
 	<input name="file" type="file" accept="image/*"/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/common/tags/messaging/index.tag b/src/web/app/common/tags/messaging/index.tag
index d38569999..24d49257c 100644
--- a/src/web/app/common/tags/messaging/index.tag
+++ b/src/web/app/common/tags/messaging/index.tag
@@ -33,7 +33,7 @@
 	</div>
 	<p class="no-history" if={ !fetching && history.length == 0 }>%i18n:common.tags.mk-messaging.no-history%</p>
 	<p class="fetching" if={ fetching }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/common/tags/messaging/message.tag b/src/web/app/common/tags/messaging/message.tag
index f211b10b5..d65bb8770 100644
--- a/src/web/app/common/tags/messaging/message.tag
+++ b/src/web/app/common/tags/messaging/message.tag
@@ -18,7 +18,7 @@
 			<mk-time time={ message.created_at }/><virtual if={ message.is_edited }>%fa:pencil-alt%</virtual>
 		</footer>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			$me-balloon-color = #23A7B6
 
diff --git a/src/web/app/common/tags/messaging/room.tag b/src/web/app/common/tags/messaging/room.tag
index 2fdf50457..0a669dbc9 100644
--- a/src/web/app/common/tags/messaging/room.tag
+++ b/src/web/app/common/tags/messaging/room.tag
@@ -16,7 +16,7 @@
 		<div class="grippie" title="%i18n:common.tags.mk-messaging-room.resize-form%"></div>
 		<mk-messaging-form user={ user }/>
 	</footer>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/common/tags/nav-links.tag b/src/web/app/common/tags/nav-links.tag
index ea122575a..3766e5c0a 100644
--- a/src/web/app/common/tags/nav-links.tag
+++ b/src/web/app/common/tags/nav-links.tag
@@ -1,6 +1,6 @@
 <mk-nav-links>
 	<a href={ aboutUrl }>%i18n:common.tags.mk-nav-links.about%</a><i>・</i><a href={ _STATS_URL_ }>%i18n:common.tags.mk-nav-links.stats%</a><i>・</i><a href={ _STATUS_URL_ }>%i18n:common.tags.mk-nav-links.status%</a><i>・</i><a href="http://zawazawa.jp/misskey/">%i18n:common.tags.mk-nav-links.wiki%</a><i>・</i><a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:common.tags.mk-nav-links.donors%</a><i>・</i><a href="https://github.com/syuilo/misskey">%i18n:common.tags.mk-nav-links.repository%</a><i>・</i><a href={ _DEV_URL_ }>%i18n:common.tags.mk-nav-links.develop%</a><i>・</i><a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on %fa:B twitter%</a>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display inline
 	</style>
diff --git a/src/web/app/common/tags/number.tag b/src/web/app/common/tags/number.tag
index 7afb8b398..4b1081a87 100644
--- a/src/web/app/common/tags/number.tag
+++ b/src/web/app/common/tags/number.tag
@@ -1,5 +1,5 @@
 <mk-number>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display inline
 	</style>
diff --git a/src/web/app/common/tags/poll-editor.tag b/src/web/app/common/tags/poll-editor.tag
index f660032c9..28e059e87 100644
--- a/src/web/app/common/tags/poll-editor.tag
+++ b/src/web/app/common/tags/poll-editor.tag
@@ -14,7 +14,7 @@
 	<button class="destroy" @click="destroy" title="%i18n:common.tags.mk-poll-editor.destroy%">
 		%fa:times%
 	</button>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			padding 8px
diff --git a/src/web/app/common/tags/poll.tag b/src/web/app/common/tags/poll.tag
index 3d0a559d0..003368815 100644
--- a/src/web/app/common/tags/poll.tag
+++ b/src/web/app/common/tags/poll.tag
@@ -15,7 +15,7 @@
 		<a if={ !isVoted } @click="toggleResult">{ result ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }</a>
 		<span if={ isVoted }>%i18n:common.tags.mk-poll.voted%</span>
 	</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/common/tags/post-menu.tag b/src/web/app/common/tags/post-menu.tag
index 2ca8c9602..da5eaf8ed 100644
--- a/src/web/app/common/tags/post-menu.tag
+++ b/src/web/app/common/tags/post-menu.tag
@@ -15,7 +15,7 @@
 			<button @click="categorize">%i18n:common.tags.mk-post-menu.categorize%</button>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		$border-color = rgba(27, 31, 35, 0.15)
 
 		:scope
diff --git a/src/web/app/common/tags/raw.tag b/src/web/app/common/tags/raw.tag
index adc6de5a3..55de0962e 100644
--- a/src/web/app/common/tags/raw.tag
+++ b/src/web/app/common/tags/raw.tag
@@ -1,5 +1,5 @@
 <mk-raw>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display inline
 	</style>
diff --git a/src/web/app/common/tags/reaction-icon.tag b/src/web/app/common/tags/reaction-icon.tag
index 012729391..50d62cfba 100644
--- a/src/web/app/common/tags/reaction-icon.tag
+++ b/src/web/app/common/tags/reaction-icon.tag
@@ -9,7 +9,7 @@
 	<virtual if={ opts.reaction == 'confused' }><img src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%"></virtual>
 	<virtual if={ opts.reaction == 'pudding' }><img src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%"></virtual>
 
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display inline
 
diff --git a/src/web/app/common/tags/reaction-picker.vue b/src/web/app/common/tags/reaction-picker.vue
index 496144d88..307b158c6 100644
--- a/src/web/app/common/tags/reaction-picker.vue
+++ b/src/web/app/common/tags/reaction-picker.vue
@@ -75,7 +75,7 @@
 			onMouseout: function(e) {
 				this.title = placeholder;
 			},
-			close: function() {
+			clo1se: function() {
 				this.$refs.backdrop.style.pointerEvents = 'none';
 				anime({
 					targets: this.$refs.backdrop,
@@ -98,89 +98,85 @@
 	};
 </script>
 
-<mk-reaction-picker>
+<style lang="stylus" scoped>
+	$border-color = rgba(27, 31, 35, 0.15)
 
-	<style>
-		$border-color = rgba(27, 31, 35, 0.15)
+	:scope
+		display block
+		position initial
 
-		:scope
-			display block
-			position initial
+		> .backdrop
+			position fixed
+			top 0
+			left 0
+			z-index 10000
+			width 100%
+			height 100%
+			background rgba(0, 0, 0, 0.1)
+			opacity 0
 
-			> .backdrop
-				position fixed
-				top 0
-				left 0
-				z-index 10000
-				width 100%
-				height 100%
-				background rgba(0, 0, 0, 0.1)
-				opacity 0
+		> .popover
+			position absolute
+			z-index 10001
+			background #fff
+			border 1px solid $border-color
+			border-radius 4px
+			box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
+			transform scale(0.5)
+			opacity 0
 
-			> .popover
-				position absolute
-				z-index 10001
-				background #fff
-				border 1px solid $border-color
-				border-radius 4px
-				box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
-				transform scale(0.5)
-				opacity 0
+			$balloon-size = 16px
 
-				$balloon-size = 16px
+			&:not(.compact)
+				margin-top $balloon-size
+				transform-origin center -($balloon-size)
 
-				&:not(.compact)
-					margin-top $balloon-size
-					transform-origin center -($balloon-size)
-
-					&:before
-						content ""
-						display block
-						position absolute
-						top -($balloon-size * 2)
-						left s('calc(50% - %s)', $balloon-size)
-						border-top solid $balloon-size transparent
-						border-left solid $balloon-size transparent
-						border-right solid $balloon-size transparent
-						border-bottom solid $balloon-size $border-color
-
-					&:after
-						content ""
-						display block
-						position absolute
-						top -($balloon-size * 2) + 1.5px
-						left s('calc(50% - %s)', $balloon-size)
-						border-top solid $balloon-size transparent
-						border-left solid $balloon-size transparent
-						border-right solid $balloon-size transparent
-						border-bottom solid $balloon-size #fff
-
-				> p
+				&:before
+					content ""
 					display block
-					margin 0
-					padding 8px 10px
-					font-size 14px
-					color #586069
-					border-bottom solid 1px #e1e4e8
+					position absolute
+					top -($balloon-size * 2)
+					left s('calc(50% - %s)', $balloon-size)
+					border-top solid $balloon-size transparent
+					border-left solid $balloon-size transparent
+					border-right solid $balloon-size transparent
+					border-bottom solid $balloon-size $border-color
 
-				> div
-					padding 4px
-					width 240px
-					text-align center
+				&:after
+					content ""
+					display block
+					position absolute
+					top -($balloon-size * 2) + 1.5px
+					left s('calc(50% - %s)', $balloon-size)
+					border-top solid $balloon-size transparent
+					border-left solid $balloon-size transparent
+					border-right solid $balloon-size transparent
+					border-bottom solid $balloon-size #fff
 
-					> button
-						width 40px
-						height 40px
-						font-size 24px
-						border-radius 2px
+			> p
+				display block
+				margin 0
+				padding 8px 10px
+				font-size 14px
+				color #586069
+				border-bottom solid 1px #e1e4e8
 
-						&:hover
-							background #eee
+			> div
+				padding 4px
+				width 240px
+				text-align center
 
-						&:active
-							background $theme-color
-							box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
+				> button
+					width 40px
+					height 40px
+					font-size 24px
+					border-radius 2px
 
-	</style>
+					&:hover
+						background #eee
 
-</mk-reaction-picker>
+					&:active
+						background $theme-color
+						box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
+
+</style>
diff --git a/src/web/app/common/tags/reactions-viewer.tag b/src/web/app/common/tags/reactions-viewer.tag
index 50fb023f7..8ec14a12f 100644
--- a/src/web/app/common/tags/reactions-viewer.tag
+++ b/src/web/app/common/tags/reactions-viewer.tag
@@ -10,7 +10,7 @@
 		<span if={ reactions.confused }><mk-reaction-icon reaction='confused'/><span>{ reactions.confused }</span></span>
 		<span if={ reactions.pudding }><mk-reaction-icon reaction='pudding'/><span>{ reactions.pudding }</span></span>
 	</virtual>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			border-top dashed 1px #eee
diff --git a/src/web/app/common/tags/signin-history.tag b/src/web/app/common/tags/signin-history.tag
index 9f02fc687..332bfdccf 100644
--- a/src/web/app/common/tags/signin-history.tag
+++ b/src/web/app/common/tags/signin-history.tag
@@ -2,7 +2,7 @@
 	<div class="records" if={ history.length != 0 }>
 		<mk-signin-record each={ rec in history } rec={ rec }/>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -50,7 +50,7 @@
 	</header>
 	<pre ref="headers" class="json" show={ show }>{ JSON.stringify(rec.headers, null, 2) }</pre>
 
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			border-bottom solid 1px #eee
diff --git a/src/web/app/common/tags/signin.tag b/src/web/app/common/tags/signin.tag
index 2ee188bbc..949217c71 100644
--- a/src/web/app/common/tags/signin.tag
+++ b/src/web/app/common/tags/signin.tag
@@ -11,7 +11,7 @@
 		</label>
 		<button type="submit" disabled={ signing }>{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }</button>
 	</form>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/common/tags/signup.tag b/src/web/app/common/tags/signup.tag
index 0b2ddf6d7..861c65201 100644
--- a/src/web/app/common/tags/signup.tag
+++ b/src/web/app/common/tags/signup.tag
@@ -38,7 +38,7 @@
 		</label>
 		<button @click="onsubmit">%i18n:common.tags.mk-signup.create%</button>
 	</form>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			min-width 302px
diff --git a/src/web/app/common/tags/special-message.tag b/src/web/app/common/tags/special-message.tag
index 6643b1324..5d62797ae 100644
--- a/src/web/app/common/tags/special-message.tag
+++ b/src/web/app/common/tags/special-message.tag
@@ -1,7 +1,7 @@
 <mk-special-message>
 	<p if={ m == 1 && d == 1 }>%i18n:common.tags.mk-special-message.new-year%</p>
 	<p if={ m == 12 && d == 25 }>%i18n:common.tags.mk-special-message.christmas%</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/common/tags/stream-indicator.vue b/src/web/app/common/tags/stream-indicator.vue
index 619237193..6964cda34 100644
--- a/src/web/app/common/tags/stream-indicator.vue
+++ b/src/web/app/common/tags/stream-indicator.vue
@@ -49,7 +49,7 @@
 	};
 </script>
 
-<style lang="stylus">
+<style lang="stylus" scoped>
 	> div
 		display block
 		pointer-events none
diff --git a/src/web/app/common/tags/twitter-setting.tag b/src/web/app/common/tags/twitter-setting.tag
index 8419f8b62..f865de466 100644
--- a/src/web/app/common/tags/twitter-setting.tag
+++ b/src/web/app/common/tags/twitter-setting.tag
@@ -7,7 +7,7 @@
 		<a href={ _API_URL_ + '/disconnect/twitter' } target="_blank" if={ I.twitter } @click="disconnect">%i18n:common.tags.mk-twitter-setting.disconnect%</a>
 	</p>
 	<p class="id" if={ I.twitter }>Twitter ID: { I.twitter.user_id }</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			color #4a535a
diff --git a/src/web/app/common/tags/uploader.tag b/src/web/app/common/tags/uploader.tag
index a95004b46..ec9ba0243 100644
--- a/src/web/app/common/tags/uploader.tag
+++ b/src/web/app/common/tags/uploader.tag
@@ -9,7 +9,7 @@
 			<div class="progress waiting" if={ progress != undefined && progress.value == progress.max }></div>
 		</li>
 	</ol>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			overflow auto
diff --git a/src/web/app/desktop/tags/analog-clock.tag b/src/web/app/desktop/tags/analog-clock.tag
index 35661405d..dda5a4b30 100644
--- a/src/web/app/desktop/tags/analog-clock.tag
+++ b/src/web/app/desktop/tags/analog-clock.tag
@@ -1,6 +1,6 @@
 <mk-analog-clock>
 	<canvas ref="canvas" width="256" height="256"></canvas>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			> canvas
 				display block
diff --git a/src/web/app/desktop/tags/autocomplete-suggestion.tag b/src/web/app/desktop/tags/autocomplete-suggestion.tag
index cf22f3a27..843b3a798 100644
--- a/src/web/app/desktop/tags/autocomplete-suggestion.tag
+++ b/src/web/app/desktop/tags/autocomplete-suggestion.tag
@@ -6,7 +6,7 @@
 			<span class="username">@{ username }</span>
 		</li>
 	</ol>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			position absolute
diff --git a/src/web/app/desktop/tags/big-follow-button.tag b/src/web/app/desktop/tags/big-follow-button.tag
index 476f95840..f2e9dc656 100644
--- a/src/web/app/desktop/tags/big-follow-button.tag
+++ b/src/web/app/desktop/tags/big-follow-button.tag
@@ -5,7 +5,7 @@
 		<virtual if={ wait }>%fa:spinner .pulse .fw%</virtual>
 	</button>
 	<div class="init" if={ init }>%fa:spinner .pulse .fw%</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/desktop/tags/contextmenu.tag b/src/web/app/desktop/tags/contextmenu.tag
index ade44fce2..09d989c09 100644
--- a/src/web/app/desktop/tags/contextmenu.tag
+++ b/src/web/app/desktop/tags/contextmenu.tag
@@ -1,6 +1,6 @@
 <mk-contextmenu>
 	<yield />
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			$width = 240px
 			$item-height = 38px
diff --git a/src/web/app/desktop/tags/crop-window.tag b/src/web/app/desktop/tags/crop-window.tag
index 80f3f4657..43bbcb8c5 100644
--- a/src/web/app/desktop/tags/crop-window.tag
+++ b/src/web/app/desktop/tags/crop-window.tag
@@ -10,7 +10,7 @@
 			</div>
 		</yield>
 	</mk-window>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/desktop/tags/detailed-post-window.tag b/src/web/app/desktop/tags/detailed-post-window.tag
index 6d6f23ac3..50b4bf920 100644
--- a/src/web/app/desktop/tags/detailed-post-window.tag
+++ b/src/web/app/desktop/tags/detailed-post-window.tag
@@ -3,7 +3,7 @@
 	<div class="main" ref="main" if={ !fetching }>
 		<mk-post-detail ref="detail" post={ post }/>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			opacity 0
diff --git a/src/web/app/desktop/tags/dialog.tag b/src/web/app/desktop/tags/dialog.tag
index aff855251..92ea0b2b1 100644
--- a/src/web/app/desktop/tags/dialog.tag
+++ b/src/web/app/desktop/tags/dialog.tag
@@ -9,7 +9,7 @@
 			</virtual>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/desktop/tags/donation.tag b/src/web/app/desktop/tags/donation.tag
index 73ee9d003..8a711890f 100644
--- a/src/web/app/desktop/tags/donation.tag
+++ b/src/web/app/desktop/tags/donation.tag
@@ -20,7 +20,7 @@
 			よろしくお願いいたします。
 		</p>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			color #fff
diff --git a/src/web/app/desktop/tags/drive/browser-window.tag b/src/web/app/desktop/tags/drive/browser-window.tag
index f49921eb6..4285992f6 100644
--- a/src/web/app/desktop/tags/drive/browser-window.tag
+++ b/src/web/app/desktop/tags/drive/browser-window.tag
@@ -8,7 +8,7 @@
 			<mk-drive-browser multiple={ true } folder={ parent.folder } ref="browser"/>
 		</yield>
 	</mk-window>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			> mk-window
 				[data-yield='header']
diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/tags/drive/browser.tag
index 7e9f4662f..0c0ce4bb4 100644
--- a/src/web/app/desktop/tags/drive/browser.tag
+++ b/src/web/app/desktop/tags/drive/browser.tag
@@ -46,7 +46,7 @@
 	<div class="dropzone" if={ draghover }></div>
 	<mk-uploader ref="uploader"/>
 	<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" onchange={ changeFileInput }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/desktop/tags/drive/file.tag b/src/web/app/desktop/tags/drive/file.tag
index 467768db1..b33e3bc5e 100644
--- a/src/web/app/desktop/tags/drive/file.tag
+++ b/src/web/app/desktop/tags/drive/file.tag
@@ -9,7 +9,7 @@
 		<img src={ file.url + '?thumbnail&size=128' } alt="" onload={ onload }/>
 	</div>
 	<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			padding 8px 0 0 0
diff --git a/src/web/app/desktop/tags/drive/folder.tag b/src/web/app/desktop/tags/drive/folder.tag
index 2fae55e50..9458671cd 100644
--- a/src/web/app/desktop/tags/drive/folder.tag
+++ b/src/web/app/desktop/tags/drive/folder.tag
@@ -1,6 +1,6 @@
 <mk-drive-browser-folder data-is-contextmenu-showing={ isContextmenuShowing.toString() } data-draghover={ draghover.toString() } @click="onclick" onmouseover={ onmouseover } onmouseout={ onmouseout } ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop } oncontextmenu={ oncontextmenu } draggable="true" ondragstart={ ondragstart } ondragend={ ondragend } title={ title }>
 	<p class="name"><virtual if={ hover }>%fa:R folder-open .fw%</virtual><virtual if={ !hover }>%fa:R folder .fw%</virtual>{ folder.name }</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			padding 8px
diff --git a/src/web/app/desktop/tags/drive/nav-folder.tag b/src/web/app/desktop/tags/drive/nav-folder.tag
index d688d2e08..f16cf2181 100644
--- a/src/web/app/desktop/tags/drive/nav-folder.tag
+++ b/src/web/app/desktop/tags/drive/nav-folder.tag
@@ -1,6 +1,6 @@
 <mk-drive-browser-nav-folder data-draghover={ draghover } @click="onclick" ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop }>
 	<virtual if={ folder == null }>%fa:cloud%</virtual><span>{ folder == null ? '%i18n:desktop.tags.mk-drive-browser-nav-folder.drive%' : folder.name }</span>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			&[data-draghover]
 				background #eee
diff --git a/src/web/app/desktop/tags/ellipsis-icon.tag b/src/web/app/desktop/tags/ellipsis-icon.tag
index 8462bfc4a..619f0d84f 100644
--- a/src/web/app/desktop/tags/ellipsis-icon.tag
+++ b/src/web/app/desktop/tags/ellipsis-icon.tag
@@ -2,7 +2,7 @@
 	<div></div>
 	<div></div>
 	<div></div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			width 70px
diff --git a/src/web/app/desktop/tags/follow-button.tag b/src/web/app/desktop/tags/follow-button.tag
index 8a1f7b2c1..5e482509a 100644
--- a/src/web/app/desktop/tags/follow-button.tag
+++ b/src/web/app/desktop/tags/follow-button.tag
@@ -5,7 +5,7 @@
 		<virtual if={ wait }>%fa:spinner .pulse .fw%</virtual>
 	</button>
 	<div class="init" if={ init }>%fa:spinner .pulse .fw%</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/desktop/tags/following-setuper.tag b/src/web/app/desktop/tags/following-setuper.tag
index d8cd32a20..9453b5bf5 100644
--- a/src/web/app/desktop/tags/following-setuper.tag
+++ b/src/web/app/desktop/tags/following-setuper.tag
@@ -12,7 +12,7 @@
 	<p class="fetching" if={ fetching }>%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
 	<a class="refresh" @click="refresh">もっと見る</a>
 	<button class="close" @click="close" title="閉じる">%fa:times%</button>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			padding 24px
diff --git a/src/web/app/desktop/tags/home-widgets/access-log.tag b/src/web/app/desktop/tags/home-widgets/access-log.tag
index ecf121d58..47a6fd350 100644
--- a/src/web/app/desktop/tags/home-widgets/access-log.tag
+++ b/src/web/app/desktop/tags/home-widgets/access-log.tag
@@ -9,7 +9,7 @@
 			<span>{ path }</span>
 		</p>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			overflow hidden
diff --git a/src/web/app/desktop/tags/home-widgets/activity.tag b/src/web/app/desktop/tags/home-widgets/activity.tag
index f2e9cf824..5cc542272 100644
--- a/src/web/app/desktop/tags/home-widgets/activity.tag
+++ b/src/web/app/desktop/tags/home-widgets/activity.tag
@@ -1,6 +1,6 @@
 <mk-activity-home-widget>
 	<mk-activity-widget design={ data.design } view={ data.view } user={ I } ref="activity"/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/desktop/tags/home-widgets/broadcast.tag b/src/web/app/desktop/tags/home-widgets/broadcast.tag
index 157c42963..a1bd2175d 100644
--- a/src/web/app/desktop/tags/home-widgets/broadcast.tag
+++ b/src/web/app/desktop/tags/home-widgets/broadcast.tag
@@ -14,7 +14,7 @@
 	}</h1>
 	<p if={ !fetching }><mk-raw if={ broadcasts.length != 0 } content={ broadcasts[i].text }/><virtual if={ broadcasts.length == 0 }>%i18n:desktop.tags.mk-broadcast-home-widget.have-a-nice-day%</virtual></p>
 	<a if={ broadcasts.length > 1 } @click="next">%i18n:desktop.tags.mk-broadcast-home-widget.next% &gt;&gt;</a>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			padding 10px
diff --git a/src/web/app/desktop/tags/home-widgets/calendar.tag b/src/web/app/desktop/tags/home-widgets/calendar.tag
index fded57e07..a304d6255 100644
--- a/src/web/app/desktop/tags/home-widgets/calendar.tag
+++ b/src/web/app/desktop/tags/home-widgets/calendar.tag
@@ -24,7 +24,7 @@
 			</div>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			padding 16px 0
diff --git a/src/web/app/desktop/tags/home-widgets/channel.tag b/src/web/app/desktop/tags/home-widgets/channel.tag
index c51ca0752..60227a629 100644
--- a/src/web/app/desktop/tags/home-widgets/channel.tag
+++ b/src/web/app/desktop/tags/home-widgets/channel.tag
@@ -7,7 +7,7 @@
 	</virtual>
 	<p class="get-started" if={ this.data.channel == null }>%i18n:desktop.tags.mk-channel-home-widget.get-started%</p>
 	<mk-channel ref="channel" show={ this.data.channel }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
@@ -110,7 +110,7 @@
 		<mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/>
 	</div>
 	<mk-channel-form ref="form"/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -207,7 +207,7 @@
 			</virtual>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
@@ -253,7 +253,7 @@
 
 <mk-channel-form>
 	<input ref="text" disabled={ wait } onkeydown={ onkeydown } placeholder="書いて">
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			width 100%
diff --git a/src/web/app/desktop/tags/home-widgets/donation.tag b/src/web/app/desktop/tags/home-widgets/donation.tag
index a51a7bebb..327cae5a0 100644
--- a/src/web/app/desktop/tags/home-widgets/donation.tag
+++ b/src/web/app/desktop/tags/home-widgets/donation.tag
@@ -3,7 +3,7 @@
 		<h1>%fa:heart%%i18n:desktop.tags.mk-donation-home-widget.title%</h1>
 		<p>{'%i18n:desktop.tags.mk-donation-home-widget.text%'.substr(0, '%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('{'))}<a href="/syuilo" data-user-preview="@syuilo">@syuilo</a>{'%i18n:desktop.tags.mk-donation-home-widget.text%'.substr('%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('}') + 1)}</p>
 	</article>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/tags/home-widgets/mentions.tag
index 5177b2db1..519e124ae 100644
--- a/src/web/app/desktop/tags/home-widgets/mentions.tag
+++ b/src/web/app/desktop/tags/home-widgets/mentions.tag
@@ -10,7 +10,7 @@
 			<virtual if={ parent.moreLoading }>%fa:spinner .pulse .fw%</virtual>
 		</yield/>
 	</mk-timeline>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/desktop/tags/home-widgets/messaging.tag b/src/web/app/desktop/tags/home-widgets/messaging.tag
index 695e1babf..53f4e2f06 100644
--- a/src/web/app/desktop/tags/home-widgets/messaging.tag
+++ b/src/web/app/desktop/tags/home-widgets/messaging.tag
@@ -3,7 +3,7 @@
 		<p class="title">%fa:comments%%i18n:desktop.tags.mk-messaging-home-widget.title%</p>
 	</virtual>
 	<mk-messaging ref="index" compact={ true }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			overflow hidden
diff --git a/src/web/app/desktop/tags/home-widgets/nav.tag b/src/web/app/desktop/tags/home-widgets/nav.tag
index 61c0b4cb5..308652433 100644
--- a/src/web/app/desktop/tags/home-widgets/nav.tag
+++ b/src/web/app/desktop/tags/home-widgets/nav.tag
@@ -1,6 +1,6 @@
 <mk-nav-home-widget>
 	<mk-nav-links/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			padding 16px
diff --git a/src/web/app/desktop/tags/home-widgets/notifications.tag b/src/web/app/desktop/tags/home-widgets/notifications.tag
index 051714eab..31ef6f608 100644
--- a/src/web/app/desktop/tags/home-widgets/notifications.tag
+++ b/src/web/app/desktop/tags/home-widgets/notifications.tag
@@ -4,7 +4,7 @@
 		<button @click="settings" title="%i18n:desktop.tags.mk-notifications-home-widget.settings%">%fa:cog%</button>
 	</virtual>
 	<mk-notifications/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/desktop/tags/home-widgets/photo-stream.tag b/src/web/app/desktop/tags/home-widgets/photo-stream.tag
index e3bf3a988..80f0573fb 100644
--- a/src/web/app/desktop/tags/home-widgets/photo-stream.tag
+++ b/src/web/app/desktop/tags/home-widgets/photo-stream.tag
@@ -9,7 +9,7 @@
 		</virtual>
 	</div>
 	<p class="empty" if={ !initializing && images.length == 0 }>%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/desktop/tags/home-widgets/post-form.tag b/src/web/app/desktop/tags/home-widgets/post-form.tag
index bf6374dd3..b20a1c361 100644
--- a/src/web/app/desktop/tags/home-widgets/post-form.tag
+++ b/src/web/app/desktop/tags/home-widgets/post-form.tag
@@ -7,7 +7,7 @@
 		<textarea disabled={ posting } ref="text" onkeydown={ onkeydown } placeholder="%i18n:desktop.tags.mk-post-form-home-widget.placeholder%"></textarea>
 		<button @click="post" disabled={ posting }>%i18n:desktop.tags.mk-post-form-home-widget.post%</button>
 	</virtual>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/desktop/tags/home-widgets/profile.tag b/src/web/app/desktop/tags/home-widgets/profile.tag
index bba5b0c47..30ca3c3b6 100644
--- a/src/web/app/desktop/tags/home-widgets/profile.tag
+++ b/src/web/app/desktop/tags/home-widgets/profile.tag
@@ -3,7 +3,7 @@
 	<img class="avatar" src={ I.avatar_url + '?thumbnail&size=96' } @click="setAvatar" alt="avatar" title="クリックでアバター編集" data-user-preview={ I.id }/>
 	<a class="name" href={ '/' + I.username }>{ I.name }</a>
 	<p class="username">@{ I.username }</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			overflow hidden
diff --git a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag b/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
index 5489edf5f..3abb35fac 100644
--- a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
+++ b/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
@@ -10,7 +10,7 @@
 	</div>
 	<p class="empty" if={ !loading && poll == null }>%i18n:desktop.tags.mk-recommended-polls-home-widget.nothing%</p>
 	<p class="loading" if={ loading }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/desktop/tags/home-widgets/rss-reader.tag b/src/web/app/desktop/tags/home-widgets/rss-reader.tag
index 45cc62a51..524d0c110 100644
--- a/src/web/app/desktop/tags/home-widgets/rss-reader.tag
+++ b/src/web/app/desktop/tags/home-widgets/rss-reader.tag
@@ -7,7 +7,7 @@
 		<virtual each={ item in items }><a href={ item.link } target="_blank">{ item.title }</a></virtual>
 	</div>
 	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/desktop/tags/home-widgets/server.tag b/src/web/app/desktop/tags/home-widgets/server.tag
index 6749a46b1..f716c9dfe 100644
--- a/src/web/app/desktop/tags/home-widgets/server.tag
+++ b/src/web/app/desktop/tags/home-widgets/server.tag
@@ -10,7 +10,7 @@
 	<mk-server-home-widget-disk if={ !initializing } show={ data.view == 3 } connection={ connection }/>
 	<mk-server-home-widget-uptimes if={ !initializing } show={ data.view == 4 } connection={ connection }/>
 	<mk-server-home-widget-info if={ !initializing } show={ data.view == 5 } connection={ connection } meta={ meta }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
@@ -158,7 +158,7 @@
 			style="stroke: none; fill: url(#{ memGradientId }); mask: url(#{ memMaskId })"/>
 		<text x="1" y="5">MEM <tspan>{ memP }%</tspan></text>
 	</svg>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -239,7 +239,7 @@
 		<p>{ cores } Cores</p>
 		<p>{ model }</p>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -297,7 +297,7 @@
 		<p>Used: { bytesToSize(used, 1) }</p>
 		<p>Free: { bytesToSize(free, 1) }</p>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -363,7 +363,7 @@
 		<p>Available: { bytesToSize(available, 1) }</p>
 		<p>Used: { bytesToSize(used, 1) }</p>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -426,7 +426,7 @@
 	<p>Uptimes</p>
 	<p>Process: { process ? process.toFixed(0) : '---' }s</p>
 	<p>OS: { os ? os.toFixed(0) : '---' }s</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			padding 10px 14px
@@ -464,7 +464,7 @@
 	<p>Maintainer: <b>{ meta.maintainer }</b></p>
 	<p>Machine: { meta.machine }</p>
 	<p>Node: { meta.node }</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			padding 10px 14px
@@ -498,7 +498,7 @@
 			riot-stroke={ color }/>
 		<text x="50%" y="50%" dy="0.05" text-anchor="middle">{ (p * 100).toFixed(0) }%</text>
 	</svg>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/desktop/tags/home-widgets/slideshow.tag b/src/web/app/desktop/tags/home-widgets/slideshow.tag
index 21b778bae..c356f5cbd 100644
--- a/src/web/app/desktop/tags/home-widgets/slideshow.tag
+++ b/src/web/app/desktop/tags/home-widgets/slideshow.tag
@@ -6,7 +6,7 @@
 		<div ref="slideB" class="slide b"></div>
 	</div>
 	<button @click="resize">%fa:expand%</button>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			overflow hidden
diff --git a/src/web/app/desktop/tags/home-widgets/timeline.tag b/src/web/app/desktop/tags/home-widgets/timeline.tag
index f44023daa..4d3d830ce 100644
--- a/src/web/app/desktop/tags/home-widgets/timeline.tag
+++ b/src/web/app/desktop/tags/home-widgets/timeline.tag
@@ -10,7 +10,7 @@
 			<virtual if={ parent.moreLoading }>%fa:spinner .pulse .fw%</virtual>
 		</yield/>
 	</mk-timeline>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/desktop/tags/home-widgets/timemachine.tag b/src/web/app/desktop/tags/home-widgets/timemachine.tag
index 3cddf5355..e47ce2d4a 100644
--- a/src/web/app/desktop/tags/home-widgets/timemachine.tag
+++ b/src/web/app/desktop/tags/home-widgets/timemachine.tag
@@ -1,6 +1,6 @@
 <mk-timemachine-home-widget>
 	<mk-calendar-widget design={ data.design } warp={ warp }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/desktop/tags/home-widgets/tips.tag b/src/web/app/desktop/tags/home-widgets/tips.tag
index 9246d0e10..2135a836c 100644
--- a/src/web/app/desktop/tags/home-widgets/tips.tag
+++ b/src/web/app/desktop/tags/home-widgets/tips.tag
@@ -1,6 +1,6 @@
 <mk-tips-home-widget>
 	<p ref="tip">%fa:R lightbulb%<span ref="text"></span></p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			overflow visible !important
diff --git a/src/web/app/desktop/tags/home-widgets/trends.tag b/src/web/app/desktop/tags/home-widgets/trends.tag
index 637f53a60..0d8454da6 100644
--- a/src/web/app/desktop/tags/home-widgets/trends.tag
+++ b/src/web/app/desktop/tags/home-widgets/trends.tag
@@ -9,7 +9,7 @@
 	</div>
 	<p class="empty" if={ !loading && post == null }>%i18n:desktop.tags.mk-trends-home-widget.nothing%</p>
 	<p class="loading" if={ loading }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
index 881373f8d..763d39449 100644
--- a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
+++ b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
@@ -15,7 +15,7 @@
 	</div>
 	<p class="empty" if={ !loading && users.length == 0 }>%i18n:desktop.tags.mk-user-recommendation-home-widget.no-one%</p>
 	<p class="loading" if={ loading }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/desktop/tags/home-widgets/version.tag b/src/web/app/desktop/tags/home-widgets/version.tag
index 2b66b0490..aeebb53b0 100644
--- a/src/web/app/desktop/tags/home-widgets/version.tag
+++ b/src/web/app/desktop/tags/home-widgets/version.tag
@@ -1,6 +1,6 @@
 <mk-version-home-widget>
 	<p>ver { _VERSION_ } (葵 aoi)</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			overflow visible !important
diff --git a/src/web/app/desktop/tags/home.tag b/src/web/app/desktop/tags/home.tag
index 204760796..e54acd18e 100644
--- a/src/web/app/desktop/tags/home.tag
+++ b/src/web/app/desktop/tags/home.tag
@@ -48,7 +48,7 @@
 			<div ref="right" data-place="right"></div>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
index 8c4234a0f..088f937e7 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -2,7 +2,7 @@
 	<virtual each={ image in images }>
 		<mk-images-image image={ image }/>
 	</virtual>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display grid
 			grid-gap 4px
@@ -60,7 +60,7 @@
 		style={ styles }
 		@click="click"
 		title={ image.name }></a>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			overflow hidden
@@ -111,7 +111,7 @@
 
 <mk-image-dialog>
 	<div class="bg" ref="bg" @click="close"></div><img ref="img" src={ image.url } alt={ image.name } title={ image.name } @click="close"/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			position fixed
diff --git a/src/web/app/desktop/tags/input-dialog.tag b/src/web/app/desktop/tags/input-dialog.tag
index 1eef25db1..26fa384e6 100644
--- a/src/web/app/desktop/tags/input-dialog.tag
+++ b/src/web/app/desktop/tags/input-dialog.tag
@@ -13,7 +13,7 @@
 			</div>
 		</yield>
 	</mk-window>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/desktop/tags/list-user.tag b/src/web/app/desktop/tags/list-user.tag
index 91a6de0a0..c0e1051d1 100644
--- a/src/web/app/desktop/tags/list-user.tag
+++ b/src/web/app/desktop/tags/list-user.tag
@@ -13,7 +13,7 @@
 		</div>
 	</div>
 	<mk-follow-button user={ user }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
diff --git a/src/web/app/desktop/tags/messaging/room-window.tag b/src/web/app/desktop/tags/messaging/room-window.tag
index 39afbe6dd..b13c2d3e9 100644
--- a/src/web/app/desktop/tags/messaging/room-window.tag
+++ b/src/web/app/desktop/tags/messaging/room-window.tag
@@ -5,7 +5,7 @@
 			<mk-messaging-room user={ parent.user }/>
 		</yield>
 	</mk-window>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			> mk-window
 				[data-yield='header']
diff --git a/src/web/app/desktop/tags/messaging/window.tag b/src/web/app/desktop/tags/messaging/window.tag
index cd756daa0..ac5513a3f 100644
--- a/src/web/app/desktop/tags/messaging/window.tag
+++ b/src/web/app/desktop/tags/messaging/window.tag
@@ -5,7 +5,7 @@
 			<mk-messaging ref="index"/>
 		</yield>
 	</mk-window>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			> mk-window
 				[data-yield='header']
diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag
index 91876c24f..99024473f 100644
--- a/src/web/app/desktop/tags/notifications.tag
+++ b/src/web/app/desktop/tags/notifications.tag
@@ -83,7 +83,7 @@
 	</button>
 	<p class="empty" if={ notifications.length == 0 && !loading }>ありません!</p>
 	<p class="loading" if={ loading }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/desktop/tags/pages/drive.tag b/src/web/app/desktop/tags/pages/drive.tag
index 1cd5ca127..12ebcc47c 100644
--- a/src/web/app/desktop/tags/pages/drive.tag
+++ b/src/web/app/desktop/tags/pages/drive.tag
@@ -1,6 +1,6 @@
 <mk-drive-page>
 	<mk-drive-browser ref="browser" folder={ opts.folder }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			position fixed
diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/tags/pages/entrance.tag
index 95acbc910..9b8b4eca6 100644
--- a/src/web/app/desktop/tags/pages/entrance.tag
+++ b/src/web/app/desktop/tags/pages/entrance.tag
@@ -28,7 +28,7 @@
 			left: 15px;
 		}
 	</style>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			$width = 1000px
 
@@ -160,7 +160,7 @@
 	<a href={ _API_URL_ + '/signin/twitter' }>Twitterでサインイン</a>
 	<div class="divider"><span>or</span></div>
 	<button class="signup" @click="parent.signup">新規登録</button><a class="introduction" @click="introduction">Misskeyについて</a>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			width 290px
@@ -296,7 +296,7 @@
 <mk-entrance-signup>
 	<mk-signup/>
 	<button class="cancel" type="button" @click="parent.signin" title="キャンセル">%fa:times%</button>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			width 368px
diff --git a/src/web/app/desktop/tags/pages/home-customize.tag b/src/web/app/desktop/tags/pages/home-customize.tag
index 457b8390e..ad74e095d 100644
--- a/src/web/app/desktop/tags/pages/home-customize.tag
+++ b/src/web/app/desktop/tags/pages/home-customize.tag
@@ -1,6 +1,6 @@
 <mk-home-customize-page>
 	<mk-home ref="home" mode="timeline" customize={ true }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/desktop/tags/pages/home.tag b/src/web/app/desktop/tags/pages/home.tag
index 62df62a48..206592518 100644
--- a/src/web/app/desktop/tags/pages/home.tag
+++ b/src/web/app/desktop/tags/pages/home.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui" page={ page }>
 		<mk-home ref="home" mode={ parent.opts.mode }/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/desktop/tags/pages/messaging-room.tag b/src/web/app/desktop/tags/pages/messaging-room.tag
index 3c21b9750..48096ec80 100644
--- a/src/web/app/desktop/tags/pages/messaging-room.tag
+++ b/src/web/app/desktop/tags/pages/messaging-room.tag
@@ -1,7 +1,7 @@
 <mk-messaging-room-page>
 	<mk-messaging-room if={ user } user={ user } is-naked={ true }/>
 
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/desktop/tags/pages/not-found.tag b/src/web/app/desktop/tags/pages/not-found.tag
index e62ea1100..f2b4ef09a 100644
--- a/src/web/app/desktop/tags/pages/not-found.tag
+++ b/src/web/app/desktop/tags/pages/not-found.tag
@@ -4,7 +4,7 @@
 			<h1>Not Found</h1>
 		</main>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/desktop/tags/pages/post.tag b/src/web/app/desktop/tags/pages/post.tag
index 6d3b030e0..43f040ed2 100644
--- a/src/web/app/desktop/tags/pages/post.tag
+++ b/src/web/app/desktop/tags/pages/post.tag
@@ -6,7 +6,7 @@
 			<a if={ parent.post.prev } href={ parent.post.prev }>%fa:angle-down%%i18n:desktop.tags.mk-post-page.prev%</a>
 		</main>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/desktop/tags/pages/search.tag b/src/web/app/desktop/tags/pages/search.tag
index ac93fdaea..4d72fad65 100644
--- a/src/web/app/desktop/tags/pages/search.tag
+++ b/src/web/app/desktop/tags/pages/search.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-search ref="search" query={ parent.opts.query }/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/desktop/tags/pages/selectdrive.tag b/src/web/app/desktop/tags/pages/selectdrive.tag
index d497a47c0..723a1dd5a 100644
--- a/src/web/app/desktop/tags/pages/selectdrive.tag
+++ b/src/web/app/desktop/tags/pages/selectdrive.tag
@@ -6,7 +6,7 @@
 		<button class="ok" @click="ok">%i18n:desktop.tags.mk-selectdrive-page.ok%</button>
 	</div>
 
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			position fixed
diff --git a/src/web/app/desktop/tags/pages/user.tag b/src/web/app/desktop/tags/pages/user.tag
index 7bad03495..8ea47408c 100644
--- a/src/web/app/desktop/tags/pages/user.tag
+++ b/src/web/app/desktop/tags/pages/user.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-user ref="user" user={ parent.user } page={ parent.opts.page }/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/tags/post-detail-sub.tag
index 2d79ddd1e..62f09d4e2 100644
--- a/src/web/app/desktop/tags/post-detail-sub.tag
+++ b/src/web/app/desktop/tags/post-detail-sub.tag
@@ -21,7 +21,7 @@
 			</div>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index 73ba930c7..4ba8275b2 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -63,7 +63,7 @@
 			</virtual>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
diff --git a/src/web/app/desktop/tags/post-form-window.tag b/src/web/app/desktop/tags/post-form-window.tag
index 8955d0679..184ff548a 100644
--- a/src/web/app/desktop/tags/post-form-window.tag
+++ b/src/web/app/desktop/tags/post-form-window.tag
@@ -15,7 +15,7 @@
 			</div>
 		</yield>
 	</mk-window>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			> mk-window
 
diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag
index 4dbc69e4e..d32a3b66f 100644
--- a/src/web/app/desktop/tags/post-form.tag
+++ b/src/web/app/desktop/tags/post-form.tag
@@ -23,7 +23,7 @@
 	</button>
 	<input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" onchange={ changeFile }/>
 	<div class="dropzone" if={ draghover }></div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			padding 16px
diff --git a/src/web/app/desktop/tags/post-preview.tag b/src/web/app/desktop/tags/post-preview.tag
index 9a7db5ffa..dcad0ff7c 100644
--- a/src/web/app/desktop/tags/post-preview.tag
+++ b/src/web/app/desktop/tags/post-preview.tag
@@ -8,7 +8,7 @@
 			</div>
 		</div>
 	</article>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
diff --git a/src/web/app/desktop/tags/progress-dialog.tag b/src/web/app/desktop/tags/progress-dialog.tag
index ef055c35b..9f7df312e 100644
--- a/src/web/app/desktop/tags/progress-dialog.tag
+++ b/src/web/app/desktop/tags/progress-dialog.tag
@@ -10,7 +10,7 @@
 			</div>
 		</yield>
 	</mk-window>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/desktop/tags/repost-form-window.tag b/src/web/app/desktop/tags/repost-form-window.tag
index b501eb076..13a862d97 100644
--- a/src/web/app/desktop/tags/repost-form-window.tag
+++ b/src/web/app/desktop/tags/repost-form-window.tag
@@ -7,7 +7,7 @@
 			<mk-repost-form ref="form" post={ parent.opts.post }/>
 		</yield>
 	</mk-window>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			> mk-window
 				[data-yield='header']
diff --git a/src/web/app/desktop/tags/repost-form.tag b/src/web/app/desktop/tags/repost-form.tag
index b2ebbf4c4..da8683ab6 100644
--- a/src/web/app/desktop/tags/repost-form.tag
+++ b/src/web/app/desktop/tags/repost-form.tag
@@ -10,7 +10,7 @@
 	<virtual if={ quote }>
 		<mk-post-form ref="form" repost={ opts.post }/>
 	</virtual>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 
 			> mk-post-preview
diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag
index 0c8dbcbf6..d263f9576 100644
--- a/src/web/app/desktop/tags/search-posts.tag
+++ b/src/web/app/desktop/tags/search-posts.tag
@@ -9,7 +9,7 @@
 			<virtual if={ parent.moreLoading }>%fa:spinner .pulse .fw%</virtual>
 		</yield/>
 	</mk-timeline>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/desktop/tags/search.tag b/src/web/app/desktop/tags/search.tag
index e29a2b273..492999181 100644
--- a/src/web/app/desktop/tags/search.tag
+++ b/src/web/app/desktop/tags/search.tag
@@ -3,7 +3,7 @@
 		<h1>{ query }</h1>
 	</header>
 	<mk-search-posts ref="posts" query={ query }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			padding-bottom 32px
diff --git a/src/web/app/desktop/tags/select-file-from-drive-window.tag b/src/web/app/desktop/tags/select-file-from-drive-window.tag
index 3e0f00c2f..8e9359b05 100644
--- a/src/web/app/desktop/tags/select-file-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-file-from-drive-window.tag
@@ -13,7 +13,7 @@
 			</div>
 		</yield>
 	</mk-window>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			> mk-window
 				[data-yield='header']
diff --git a/src/web/app/desktop/tags/select-folder-from-drive-window.tag b/src/web/app/desktop/tags/select-folder-from-drive-window.tag
index ad4ae4caf..317fb90ad 100644
--- a/src/web/app/desktop/tags/select-folder-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-folder-from-drive-window.tag
@@ -11,7 +11,7 @@
 			</div>
 		</yield>
 	</mk-window>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			> mk-window
 				[data-yield='header']
diff --git a/src/web/app/desktop/tags/set-avatar-suggestion.tag b/src/web/app/desktop/tags/set-avatar-suggestion.tag
index 82a438fb7..923871a79 100644
--- a/src/web/app/desktop/tags/set-avatar-suggestion.tag
+++ b/src/web/app/desktop/tags/set-avatar-suggestion.tag
@@ -2,7 +2,7 @@
 	<p><b>アバターを設定</b>してみませんか?
 		<button @click="close">%fa:times%</button>
 	</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			cursor pointer
diff --git a/src/web/app/desktop/tags/set-banner-suggestion.tag b/src/web/app/desktop/tags/set-banner-suggestion.tag
index c5c5c7019..fa4e5843b 100644
--- a/src/web/app/desktop/tags/set-banner-suggestion.tag
+++ b/src/web/app/desktop/tags/set-banner-suggestion.tag
@@ -2,7 +2,7 @@
 	<p><b>バナーを設定</b>してみませんか?
 		<button @click="close">%fa:times%</button>
 	</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			cursor pointer
diff --git a/src/web/app/desktop/tags/settings-window.tag b/src/web/app/desktop/tags/settings-window.tag
index 09566b898..64ce1336d 100644
--- a/src/web/app/desktop/tags/settings-window.tag
+++ b/src/web/app/desktop/tags/settings-window.tag
@@ -5,7 +5,7 @@
 			<mk-settings/>
 		</yield>
 	</mk-window>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			> mk-window
 				[data-yield='header']
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 084bde009..211e36741 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -67,7 +67,7 @@
 			%license%
 		</section>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display flex
 			width 100%
@@ -150,7 +150,7 @@
 		<input ref="accountBirthday" type="date" value={ I.profile.birthday } class="ui"/>
 	</label>
 	<button class="ui primary" @click="updateAccount">%i18n:desktop.tags.mk-profile-setting.save%</button>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -196,7 +196,7 @@
 	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-api-info.caution%</p></div>
 	<p>%i18n:desktop.tags.mk-api-info.regeneration-of-token%</p>
 	<button class="ui" @click="regenerateToken">%i18n:desktop.tags.mk-api-info.regenerate-token%</button>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			color #4a535a
@@ -226,7 +226,7 @@
 
 <mk-password-setting>
 	<button @click="reset" class="ui primary">%i18n:desktop.tags.mk-password-setting.reset%</button>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			color #4a535a
@@ -281,7 +281,7 @@
 		</ol>
 		<div class="ui info"><p>%fa:info-circle%%i18n:desktop.tags.mk-2fa-setting.info%</p></div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			color #4a535a
@@ -351,7 +351,7 @@
 		<text x="50%" y="50%" dy="0.05" text-anchor="middle">{ (usageP * 100).toFixed(0) }%</text>
 	</svg>
 
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			color #4a535a
@@ -403,7 +403,7 @@
 		</div>
 	</div>
 
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/desktop/tags/sub-post-content.tag b/src/web/app/desktop/tags/sub-post-content.tag
index 01e1fdb31..a07180b67 100644
--- a/src/web/app/desktop/tags/sub-post-content.tag
+++ b/src/web/app/desktop/tags/sub-post-content.tag
@@ -14,7 +14,7 @@
 		<summary>投票</summary>
 		<mk-poll post={ post }/>
 	</details>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			overflow-wrap break-word
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 115b22c86..008c69017 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -6,7 +6,7 @@
 	<footer data-yield="footer">
 		<yield from="footer"/>
 	</footer>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -151,7 +151,7 @@
 	<div class="detail" if={ isDetailOpened }>
 		<mk-post-status-graph width="462" height="130" post={ p }/>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
@@ -613,7 +613,7 @@
 			</div>
 		</div>
 	</article>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index 4b302a0eb..cae30dbe2 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -6,7 +6,7 @@
 		<yield />
 	</div>
 	<mk-stream-indicator if={ SIGNIN }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
@@ -56,7 +56,7 @@
 			</div>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			position -webkit-sticky
@@ -128,7 +128,7 @@
 		<input ref="q" type="search" placeholder="%i18n:desktop.tags.mk-ui-header-search.placeholder%"/>
 		<div class="result"></div>
 	</form>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 
 			> form
@@ -187,7 +187,7 @@
 
 <mk-ui-header-post-button>
 	<button @click="post" title="%i18n:desktop.tags.mk-ui-header-post-button.post%">%fa:pencil-alt%</button>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display inline-block
 			padding 8px
@@ -235,7 +235,7 @@
 	<div class="notifications" if={ isOpen }>
 		<mk-notifications/>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			float left
@@ -420,7 +420,7 @@
 			</a>
 		</li>
 	</ul>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display inline-block
 			margin 0
@@ -552,7 +552,7 @@
 	<div class="content">
 		<mk-analog-clock/>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display inline-block
 			overflow visible
@@ -656,7 +656,7 @@
 			</li>
 		</ul>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			float left
@@ -845,7 +845,7 @@
 
 <mk-ui-notification>
 	<p>{ opts.message }</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			position fixed
diff --git a/src/web/app/desktop/tags/user-followers-window.tag b/src/web/app/desktop/tags/user-followers-window.tag
index 43127a68a..a67888fa7 100644
--- a/src/web/app/desktop/tags/user-followers-window.tag
+++ b/src/web/app/desktop/tags/user-followers-window.tag
@@ -3,7 +3,7 @@
 <yield to="content">
 		<mk-user-followers user={ parent.user }/></yield>
 	</mk-window>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			> mk-window
 				[data-yield='header']
diff --git a/src/web/app/desktop/tags/user-followers.tag b/src/web/app/desktop/tags/user-followers.tag
index ea670e272..79fa87141 100644
--- a/src/web/app/desktop/tags/user-followers.tag
+++ b/src/web/app/desktop/tags/user-followers.tag
@@ -1,6 +1,6 @@
 <mk-user-followers>
 	<mk-users-list fetch={ fetch } count={ user.followers_count } you-know-count={ user.followers_you_know_count } no-users={ 'フォロワーはいないようです。' }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			height 100%
diff --git a/src/web/app/desktop/tags/user-following-window.tag b/src/web/app/desktop/tags/user-following-window.tag
index 10a84db31..dd798a020 100644
--- a/src/web/app/desktop/tags/user-following-window.tag
+++ b/src/web/app/desktop/tags/user-following-window.tag
@@ -3,7 +3,7 @@
 <yield to="content">
 		<mk-user-following user={ parent.user }/></yield>
 	</mk-window>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			> mk-window
 				[data-yield='header']
diff --git a/src/web/app/desktop/tags/user-following.tag b/src/web/app/desktop/tags/user-following.tag
index 4523beac2..260900f95 100644
--- a/src/web/app/desktop/tags/user-following.tag
+++ b/src/web/app/desktop/tags/user-following.tag
@@ -1,6 +1,6 @@
 <mk-user-following>
 	<mk-users-list fetch={ fetch } count={ user.following_count } you-know-count={ user.following_you_know_count } no-users={ 'フォロー中のユーザーはいないようです。' }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			height 100%
diff --git a/src/web/app/desktop/tags/user-preview.tag b/src/web/app/desktop/tags/user-preview.tag
index 7993895a8..cf7b96275 100644
--- a/src/web/app/desktop/tags/user-preview.tag
+++ b/src/web/app/desktop/tags/user-preview.tag
@@ -19,7 +19,7 @@
 		</div>
 		<mk-follow-button if={ SIGNIN && user.id != I.id } user={ userPromise }/>
 	</virtual>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			position absolute
diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/tags/user-timeline.tag
index 0bfad05c2..be2649fb6 100644
--- a/src/web/app/desktop/tags/user-timeline.tag
+++ b/src/web/app/desktop/tags/user-timeline.tag
@@ -12,7 +12,7 @@
 			<virtual if={ parent.moreLoading }>%fa:spinner .pulse .fw%</virtual>
 		</yield/>
 	</mk-timeline>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/tags/user.tag
index 8eca3caaa..046fef681 100644
--- a/src/web/app/desktop/tags/user.tag
+++ b/src/web/app/desktop/tags/user.tag
@@ -6,7 +6,7 @@
 		<mk-user-home if={ page == 'home' } user={ user }/>
 		<mk-user-graphs if={ page == 'graphs' } user={ user }/>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -56,7 +56,7 @@
 			<a href={ '/' + user.username + '/graphs' } data-active={ parent.page == 'graphs' }>%fa:chart-bar%グラフ</a>
 		</footer>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			$banner-height = 320px
 			$footer-height = 58px
@@ -242,7 +242,7 @@
 		<p class="following">%fa:angle-right%<a @click="showFollowing">{ user.following_count }</a>人を<b>フォロー</b></p>
 		<p class="followers">%fa:angle-right%<a @click="showFollowers">{ user.followers_count }</a>人の<b>フォロワー</b></p>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
@@ -362,7 +362,7 @@
 		</virtual>
 	</div>
 	<p class="empty" if={ !initializing && images.length == 0 }>%i18n:desktop.tags.mk-user.photos.no-photos%</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
@@ -461,7 +461,7 @@
 		<mk-follow-button user={ _user }/>
 	</div>
 	<p class="empty" if={ !initializing && users.length == 0 }>%i18n:desktop.tags.mk-user.frequently-replied-users.no-users%</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
@@ -568,7 +568,7 @@
 	</virtual>
 	</div>
 	<p class="empty" if={ !initializing && users.length == 0 }>%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
@@ -654,7 +654,7 @@
 			<div class="nav"><mk-nav-links/></div>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display flex
 			justify-content center
@@ -753,7 +753,7 @@
 			<mk-user-likes-graph user={ opts.user }/>
 		</div>
 	</section>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -806,7 +806,7 @@
 	</p>
 	<p>* 中央値</p>
 
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/desktop/tags/users-list.tag b/src/web/app/desktop/tags/users-list.tag
index 3e993a40e..fd5c73b7d 100644
--- a/src/web/app/desktop/tags/users-list.tag
+++ b/src/web/app/desktop/tags/users-list.tag
@@ -16,7 +16,7 @@
 	</button>
 	<p class="no" if={ !fetching && users.length == 0 }>{ opts.noUsers }</p>
 	<p class="fetching" if={ fetching }>%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			height 100%
diff --git a/src/web/app/desktop/tags/widgets/activity.tag b/src/web/app/desktop/tags/widgets/activity.tag
index 9b547b95f..b9132e5a5 100644
--- a/src/web/app/desktop/tags/widgets/activity.tag
+++ b/src/web/app/desktop/tags/widgets/activity.tag
@@ -6,7 +6,7 @@
 	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<mk-activity-widget-calender if={ !initializing && view == 0 } data={ [].concat(activity) }/>
 	<mk-activity-widget-chart if={ !initializing && view == 1 } data={ [].concat(activity) }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
@@ -110,7 +110,7 @@
 			stroke-width="0.1"
 			stroke="#f73520"/>
 	</svg>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -174,7 +174,7 @@
 			stroke="#555"
 			stroke-dasharray="2 2"/>
 	</svg>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/desktop/tags/widgets/calendar.tag b/src/web/app/desktop/tags/widgets/calendar.tag
index 00205a90a..4e365650c 100644
--- a/src/web/app/desktop/tags/widgets/calendar.tag
+++ b/src/web/app/desktop/tags/widgets/calendar.tag
@@ -18,7 +18,7 @@
 				@click="go.bind(null, i + 1)"
 				title={ isOutOfRange(i + 1) ? null : '%i18n:desktop.tags.mk-calendar-widget.go%' }><div>{ i + 1 }</div></div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			color #777
diff --git a/src/web/app/desktop/tags/window.tag b/src/web/app/desktop/tags/window.tag
index 31830d907..3752f3609 100644
--- a/src/web/app/desktop/tags/window.tag
+++ b/src/web/app/desktop/tags/window.tag
@@ -20,7 +20,7 @@
 		<div class="handle bottom-right" if={ canResize } onmousedown={ onBottomRightHandleMousedown }></div>
 		<div class="handle bottom-left" if={ canResize } onmousedown={ onBottomLeftHandleMousedown }></div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/dev/tags/new-app-form.tag b/src/web/app/dev/tags/new-app-form.tag
index aba6b1524..1bd5b5a83 100644
--- a/src/web/app/dev/tags/new-app-form.tag
+++ b/src/web/app/dev/tags/new-app-form.tag
@@ -75,7 +75,7 @@
 		</section>
 		<button @click="onsubmit">アプリ作成</button>
 	</form>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			overflow hidden
diff --git a/src/web/app/dev/tags/pages/app.tag b/src/web/app/dev/tags/pages/app.tag
index b25e0d859..3fdf8d15b 100644
--- a/src/web/app/dev/tags/pages/app.tag
+++ b/src/web/app/dev/tags/pages/app.tag
@@ -9,7 +9,7 @@
 			<input value={ app.secret } readonly="readonly"/>
 		</div>
 	</main>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/dev/tags/pages/apps.tag b/src/web/app/dev/tags/pages/apps.tag
index 43db70fcf..fbacee137 100644
--- a/src/web/app/dev/tags/pages/apps.tag
+++ b/src/web/app/dev/tags/pages/apps.tag
@@ -10,7 +10,7 @@
 			</ul>
 		</virtual>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/dev/tags/pages/index.tag b/src/web/app/dev/tags/pages/index.tag
index f863876fa..ca270b377 100644
--- a/src/web/app/dev/tags/pages/index.tag
+++ b/src/web/app/dev/tags/pages/index.tag
@@ -1,5 +1,5 @@
 <mk-index><a href="/apps">アプリ</a>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/dev/tags/pages/new-app.tag b/src/web/app/dev/tags/pages/new-app.tag
index 238b6865e..26185f278 100644
--- a/src/web/app/dev/tags/pages/new-app.tag
+++ b/src/web/app/dev/tags/pages/new-app.tag
@@ -6,7 +6,7 @@
 		</header>
 		<mk-new-app-form/>
 	</main>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			padding 64px 0
diff --git a/src/web/app/mobile/tags/drive-folder-selector.tag b/src/web/app/mobile/tags/drive-folder-selector.tag
index 6a0cb5cea..94cf1db41 100644
--- a/src/web/app/mobile/tags/drive-folder-selector.tag
+++ b/src/web/app/mobile/tags/drive-folder-selector.tag
@@ -7,7 +7,7 @@
 		</header>
 		<mk-drive ref="browser" select-folder={ true }/>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			position fixed
diff --git a/src/web/app/mobile/tags/drive-selector.tag b/src/web/app/mobile/tags/drive-selector.tag
index 9e6f6a045..9c3a4b5c4 100644
--- a/src/web/app/mobile/tags/drive-selector.tag
+++ b/src/web/app/mobile/tags/drive-selector.tag
@@ -7,7 +7,7 @@
 		</header>
 		<mk-drive ref="browser" select-file={ true } multiple={ opts.multiple }/>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			position fixed
diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index 3d0396692..a063d0ca6 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -51,7 +51,7 @@
 	</div>
 	<input ref="file" type="file" multiple="multiple" onchange={ changeLocalFile }/>
 	<mk-drive-file-viewer if={ file != null } file={ file }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag
index 82fbb6609..119ad1fb2 100644
--- a/src/web/app/mobile/tags/drive/file-viewer.tag
+++ b/src/web/app/mobile/tags/drive/file-viewer.tag
@@ -60,7 +60,7 @@
 			<code>{ file.md5 }</code>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag
index a04528ce7..96754e1b3 100644
--- a/src/web/app/mobile/tags/drive/file.tag
+++ b/src/web/app/mobile/tags/drive/file.tag
@@ -22,7 +22,7 @@
 			</div>
 		</div>
 	</a>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/mobile/tags/drive/folder.tag b/src/web/app/mobile/tags/drive/folder.tag
index c0ccee6a5..bb17c5e67 100644
--- a/src/web/app/mobile/tags/drive/folder.tag
+++ b/src/web/app/mobile/tags/drive/folder.tag
@@ -4,7 +4,7 @@
 			<p class="name">%fa:folder%{ folder.name }</p>%fa:angle-right%
 		</div>
 	</a>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/mobile/tags/follow-button.tag b/src/web/app/mobile/tags/follow-button.tag
index 805d5e659..baf8f2ffa 100644
--- a/src/web/app/mobile/tags/follow-button.tag
+++ b/src/web/app/mobile/tags/follow-button.tag
@@ -5,7 +5,7 @@
 		<virtual if={ wait }>%fa:spinner .pulse .fw%</virtual>{ user.is_following ? '%i18n:mobile.tags.mk-follow-button.unfollow%' : '%i18n:mobile.tags.mk-follow-button.follow%' }
 	</button>
 	<div class="init" if={ init }>%fa:spinner .pulse .fw%</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag
index aa3818007..86708bfee 100644
--- a/src/web/app/mobile/tags/home-timeline.tag
+++ b/src/web/app/mobile/tags/home-timeline.tag
@@ -1,7 +1,7 @@
 <mk-home-timeline>
 	<mk-init-following if={ noFollowing } />
 	<mk-timeline ref="timeline" init={ init } more={ more } empty={ '%i18n:mobile.tags.mk-home-timeline.empty-timeline%' }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/mobile/tags/home.tag b/src/web/app/mobile/tags/home.tag
index 2c07c286d..1bb9027dd 100644
--- a/src/web/app/mobile/tags/home.tag
+++ b/src/web/app/mobile/tags/home.tag
@@ -1,6 +1,6 @@
 <mk-home>
 	<mk-home-timeline ref="tl"/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/mobile/tags/images.tag b/src/web/app/mobile/tags/images.tag
index 5899364ae..c39eda38b 100644
--- a/src/web/app/mobile/tags/images.tag
+++ b/src/web/app/mobile/tags/images.tag
@@ -2,7 +2,7 @@
 	<virtual each={ image in images }>
 		<mk-images-image image={ image }/>
 	</virtual>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display grid
 			grid-gap 4px
@@ -57,7 +57,7 @@
 
 <mk-images-image>
 	<a ref="view" href={ image.url } target="_blank" style={ styles } title={ image.name }></a>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			overflow hidden
diff --git a/src/web/app/mobile/tags/init-following.tag b/src/web/app/mobile/tags/init-following.tag
index d7e31b460..e0e2532af 100644
--- a/src/web/app/mobile/tags/init-following.tag
+++ b/src/web/app/mobile/tags/init-following.tag
@@ -9,7 +9,7 @@
 	<p class="fetching" if={ fetching }>%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
 	<a class="refresh" @click="refresh">もっと見る</a>
 	<button class="close" @click="close" title="閉じる">%fa:times%</button>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
diff --git a/src/web/app/mobile/tags/notification-preview.tag b/src/web/app/mobile/tags/notification-preview.tag
index ab923ea9d..b2064cd42 100644
--- a/src/web/app/mobile/tags/notification-preview.tag
+++ b/src/web/app/mobile/tags/notification-preview.tag
@@ -47,7 +47,7 @@
 			<p class="post-ref">%fa:quote-left%{ getPostSummary(notification.post) }%fa:quote-right%</p>
 		</div>
 	</virtual>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
diff --git a/src/web/app/mobile/tags/notification.tag b/src/web/app/mobile/tags/notification.tag
index de44caea2..23a9f2fe3 100644
--- a/src/web/app/mobile/tags/notification.tag
+++ b/src/web/app/mobile/tags/notification.tag
@@ -89,7 +89,7 @@
 			</a>
 		</div>
 	</virtual>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index 520a336b0..ade71ea40 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -10,7 +10,7 @@
 	</button>
 	<p class="empty" if={ notifications.length == 0 && !loading }>%i18n:mobile.tags.mk-notifications.empty%</p>
 	<p class="loading" if={ loading }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 8px auto
diff --git a/src/web/app/mobile/tags/notify.tag b/src/web/app/mobile/tags/notify.tag
index 386166f7f..787d3a374 100644
--- a/src/web/app/mobile/tags/notify.tag
+++ b/src/web/app/mobile/tags/notify.tag
@@ -1,6 +1,6 @@
 <mk-notify>
 	<mk-notification-preview notification={ opts.notification }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			position fixed
diff --git a/src/web/app/mobile/tags/page/drive.tag b/src/web/app/mobile/tags/page/drive.tag
index b5ed3385e..8cc8134bc 100644
--- a/src/web/app/mobile/tags/page/drive.tag
+++ b/src/web/app/mobile/tags/page/drive.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-drive ref="browser" folder={ parent.opts.folder } file={ parent.opts.file } is-naked={ true } top={ 48 }/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/mobile/tags/page/entrance.tag b/src/web/app/mobile/tags/page/entrance.tag
index b5da3c947..ebcf30f80 100644
--- a/src/web/app/mobile/tags/page/entrance.tag
+++ b/src/web/app/mobile/tags/page/entrance.tag
@@ -10,7 +10,7 @@
 	<footer>
 		<p class="c">{ _COPYRIGHT_ }</p>
 	</footer>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			height 100%
diff --git a/src/web/app/mobile/tags/page/entrance/signin.tag b/src/web/app/mobile/tags/page/entrance/signin.tag
index 81d0a48a7..e6deea8c3 100644
--- a/src/web/app/mobile/tags/page/entrance/signin.tag
+++ b/src/web/app/mobile/tags/page/entrance/signin.tag
@@ -3,7 +3,7 @@
 	<a href={ _API_URL_ + '/signin/twitter' }>Twitterでサインイン</a>
 	<div class="divider"><span>or</span></div>
 	<button class="signup" @click="parent.signup">%i18n:mobile.tags.mk-entrance-signin.signup%</button><a class="introduction" @click="parent.introduction">%i18n:mobile.tags.mk-entrance-signin.about%</a>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0 auto
diff --git a/src/web/app/mobile/tags/page/entrance/signup.tag b/src/web/app/mobile/tags/page/entrance/signup.tag
index 6634593d3..d219bb100 100644
--- a/src/web/app/mobile/tags/page/entrance/signup.tag
+++ b/src/web/app/mobile/tags/page/entrance/signup.tag
@@ -1,7 +1,7 @@
 <mk-entrance-signup>
 	<mk-signup/>
 	<button class="cancel" type="button" @click="parent.signin" title="%i18n:mobile.tags.mk-entrance-signup.cancel%">%fa:times%</button>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0 auto
diff --git a/src/web/app/mobile/tags/page/home.tag b/src/web/app/mobile/tags/page/home.tag
index 0c3121a21..4b9343a10 100644
--- a/src/web/app/mobile/tags/page/home.tag
+++ b/src/web/app/mobile/tags/page/home.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-home ref="home"/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/mobile/tags/page/messaging-room.tag b/src/web/app/mobile/tags/page/messaging-room.tag
index 00ee26512..075ea8e83 100644
--- a/src/web/app/mobile/tags/page/messaging-room.tag
+++ b/src/web/app/mobile/tags/page/messaging-room.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-messaging-room if={ !parent.fetching } user={ parent.user } is-naked={ true }/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/mobile/tags/page/messaging.tag b/src/web/app/mobile/tags/page/messaging.tag
index 76d610377..acde6f269 100644
--- a/src/web/app/mobile/tags/page/messaging.tag
+++ b/src/web/app/mobile/tags/page/messaging.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-messaging ref="index"/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/mobile/tags/page/new-post.tag b/src/web/app/mobile/tags/page/new-post.tag
index 7adde3b32..1650446b4 100644
--- a/src/web/app/mobile/tags/page/new-post.tag
+++ b/src/web/app/mobile/tags/page/new-post.tag
@@ -1,6 +1,6 @@
 <mk-new-post-page>
 	<mk-post-form ref="form"/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/mobile/tags/page/notifications.tag b/src/web/app/mobile/tags/page/notifications.tag
index 596467d47..97717e2e2 100644
--- a/src/web/app/mobile/tags/page/notifications.tag
+++ b/src/web/app/mobile/tags/page/notifications.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-notifications ref="notifications"/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/mobile/tags/page/post.tag b/src/web/app/mobile/tags/page/post.tag
index 5303ca8d3..003f9dea5 100644
--- a/src/web/app/mobile/tags/page/post.tag
+++ b/src/web/app/mobile/tags/page/post.tag
@@ -8,7 +8,7 @@
 			<a if={ parent.post.prev } href={ parent.post.prev }>%fa:angle-down%%i18n:mobile.tags.mk-post-page.prev%</a>
 		</main>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/mobile/tags/page/search.tag b/src/web/app/mobile/tags/page/search.tag
index 51c8cce8b..393076367 100644
--- a/src/web/app/mobile/tags/page/search.tag
+++ b/src/web/app/mobile/tags/page/search.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-search ref="search" query={ parent.opts.query }/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/mobile/tags/page/selectdrive.tag b/src/web/app/mobile/tags/page/selectdrive.tag
index 172a161ec..c7ff66d05 100644
--- a/src/web/app/mobile/tags/page/selectdrive.tag
+++ b/src/web/app/mobile/tags/page/selectdrive.tag
@@ -6,7 +6,7 @@
 	</header>
 	<mk-drive ref="browser" select-file={ true } multiple={ multiple } is-naked={ true } top={ 42 }/>
 
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			width 100%
diff --git a/src/web/app/mobile/tags/page/settings.tag b/src/web/app/mobile/tags/page/settings.tag
index b388121cb..beaa08b9a 100644
--- a/src/web/app/mobile/tags/page/settings.tag
+++ b/src/web/app/mobile/tags/page/settings.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-settings />
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
@@ -29,7 +29,7 @@
 		<li><a @click="signout">%fa:power-off%%i18n:mobile.tags.mk-settings-page.signout%</a></li>
 	</ul>
 	<p><small>ver { _VERSION_ } (葵 aoi)</small></p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/mobile/tags/page/settings/authorized-apps.tag b/src/web/app/mobile/tags/page/settings/authorized-apps.tag
index 8d538eba5..0145afc62 100644
--- a/src/web/app/mobile/tags/page/settings/authorized-apps.tag
+++ b/src/web/app/mobile/tags/page/settings/authorized-apps.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-authorized-apps/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
index 5d6c47794..e213f4070 100644
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-profile-setting/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
@@ -51,7 +51,7 @@
 		</div>
 		<button class="save" @click="save" disabled={ saving }>%fa:check%%i18n:mobile.tags.mk-profile-setting.save%</button>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/mobile/tags/page/settings/signin.tag b/src/web/app/mobile/tags/page/settings/signin.tag
index 1a9e63886..5c9164bcf 100644
--- a/src/web/app/mobile/tags/page/settings/signin.tag
+++ b/src/web/app/mobile/tags/page/settings/signin.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-signin-history/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/mobile/tags/page/settings/twitter.tag b/src/web/app/mobile/tags/page/settings/twitter.tag
index 02661d3b6..672eff25b 100644
--- a/src/web/app/mobile/tags/page/settings/twitter.tag
+++ b/src/web/app/mobile/tags/page/settings/twitter.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-twitter-setting/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/mobile/tags/page/user-followers.tag b/src/web/app/mobile/tags/page/user-followers.tag
index a5e63613c..50280e7b9 100644
--- a/src/web/app/mobile/tags/page/user-followers.tag
+++ b/src/web/app/mobile/tags/page/user-followers.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-user-followers ref="list" if={ !parent.fetching } user={ parent.user }/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/mobile/tags/page/user-following.tag b/src/web/app/mobile/tags/page/user-following.tag
index b4ed10783..b28efbab9 100644
--- a/src/web/app/mobile/tags/page/user-following.tag
+++ b/src/web/app/mobile/tags/page/user-following.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-user-following ref="list" if={ !parent.fetching } user={ parent.user }/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/mobile/tags/page/user.tag b/src/web/app/mobile/tags/page/user.tag
index 8eec733fc..04b727636 100644
--- a/src/web/app/mobile/tags/page/user.tag
+++ b/src/web/app/mobile/tags/page/user.tag
@@ -2,7 +2,7 @@
 	<mk-ui ref="ui">
 		<mk-user ref="user" user={ parent.user } page={ parent.opts.page }/>
 	</mk-ui>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index be377d77f..e397ce7c0 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -62,7 +62,7 @@
 			<mk-post-detail-sub post={ post }/>
 		</virtual>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			overflow hidden
@@ -367,7 +367,7 @@
 			</div>
 		</div>
 	</article>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
index 6f0794753..202b03c20 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -24,7 +24,7 @@
 		<button class="poll" @click="addPoll">%fa:chart-pie%</button>
 		<input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			max-width 500px
diff --git a/src/web/app/mobile/tags/post-preview.tag b/src/web/app/mobile/tags/post-preview.tag
index aaf846703..716916587 100644
--- a/src/web/app/mobile/tags/post-preview.tag
+++ b/src/web/app/mobile/tags/post-preview.tag
@@ -16,7 +16,7 @@
 			</div>
 		</div>
 	</article>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag
index 3e3c034f2..9cb5ee36f 100644
--- a/src/web/app/mobile/tags/search-posts.tag
+++ b/src/web/app/mobile/tags/search-posts.tag
@@ -1,6 +1,6 @@
 <mk-search-posts>
 	<mk-timeline init={ init } more={ more } empty={ '%i18n:mobile.tags.mk-search-posts.empty%'.replace('{}', query) }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 8px auto
diff --git a/src/web/app/mobile/tags/search.tag b/src/web/app/mobile/tags/search.tag
index 15a861d7a..ab048ea13 100644
--- a/src/web/app/mobile/tags/search.tag
+++ b/src/web/app/mobile/tags/search.tag
@@ -1,6 +1,6 @@
 <mk-search>
 	<mk-search-posts ref="posts" query={ query }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
diff --git a/src/web/app/mobile/tags/sub-post-content.tag b/src/web/app/mobile/tags/sub-post-content.tag
index 7192cd013..3d9175b18 100644
--- a/src/web/app/mobile/tags/sub-post-content.tag
+++ b/src/web/app/mobile/tags/sub-post-content.tag
@@ -8,7 +8,7 @@
 		<summary>%i18n:mobile.tags.mk-sub-post-content.poll%</summary>
 		<mk-poll post={ post }/>
 	</details>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			overflow-wrap break-word
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 66f58ff0a..3daf6b6d1 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -18,7 +18,7 @@
 			<span if={ fetching }>%i18n:common.loading%<mk-ellipsis/></span>
 		</button>
 	</footer>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			background #fff
@@ -197,7 +197,7 @@
 			</footer>
 		</div>
 	</article>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
@@ -595,7 +595,7 @@
 			</div>
 		</div>
 	</article>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index c5dc4b2e4..8f0324f4d 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -5,7 +5,7 @@
 		<yield />
 	</div>
 	<mk-stream-indicator if={ SIGNIN }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			padding-top 48px
@@ -58,7 +58,7 @@
 			<button if={ func } @click="func"><mk-raw content={ funcIcon }/></button>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			$height = 48px
 
@@ -250,7 +250,7 @@
 		</div>
 		<a href={ aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display none
 
diff --git a/src/web/app/mobile/tags/user-card.tag b/src/web/app/mobile/tags/user-card.tag
index d0c79698c..abe46bda0 100644
--- a/src/web/app/mobile/tags/user-card.tag
+++ b/src/web/app/mobile/tags/user-card.tag
@@ -7,7 +7,7 @@
 	<a class="name" href={ '/' + user.username } target="_blank">{ user.name }</a>
 	<p class="username">@{ user.username }</p>
 	<mk-follow-button user={ user }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display inline-block
 			width 200px
diff --git a/src/web/app/mobile/tags/user-followers.tag b/src/web/app/mobile/tags/user-followers.tag
index c4cdedba8..a4dc99e68 100644
--- a/src/web/app/mobile/tags/user-followers.tag
+++ b/src/web/app/mobile/tags/user-followers.tag
@@ -1,6 +1,6 @@
 <mk-user-followers>
 	<mk-users-list ref="list" fetch={ fetch } count={ user.followers_count } you-know-count={ user.followers_you_know_count } no-users={ '%i18n:mobile.tags.mk-user-followers.no-users%' }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/mobile/tags/user-following.tag b/src/web/app/mobile/tags/user-following.tag
index 3a6a54dd7..e1d98297c 100644
--- a/src/web/app/mobile/tags/user-following.tag
+++ b/src/web/app/mobile/tags/user-following.tag
@@ -1,6 +1,6 @@
 <mk-user-following>
 	<mk-users-list ref="list" fetch={ fetch } count={ user.following_count } you-know-count={ user.following_you_know_count } no-users={ '%i18n:mobile.tags.mk-user-following.no-users%' }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/mobile/tags/user-preview.tag b/src/web/app/mobile/tags/user-preview.tag
index 48bf88a89..498ad53ec 100644
--- a/src/web/app/mobile/tags/user-preview.tag
+++ b/src/web/app/mobile/tags/user-preview.tag
@@ -11,7 +11,7 @@
 			<div class="description">{ user.description }</div>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0
diff --git a/src/web/app/mobile/tags/user-timeline.tag b/src/web/app/mobile/tags/user-timeline.tag
index 65203fec4..dd878810c 100644
--- a/src/web/app/mobile/tags/user-timeline.tag
+++ b/src/web/app/mobile/tags/user-timeline.tag
@@ -1,6 +1,6 @@
 <mk-user-timeline>
 	<mk-timeline ref="timeline" init={ init } more={ more } empty={ withMedia ? '%i18n:mobile.tags.mk-user-timeline.no-posts-with-media%' : '%i18n:mobile.tags.mk-user-timeline.no-posts%' }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			max-width 600px
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index eb6d9ffbe..f0ecbd1c3 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -50,7 +50,7 @@
 			<mk-user-timeline if={ page == 'media' } user={ user } with-media={ true }/>
 		</div>
 	</div>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -259,7 +259,7 @@
 		</div>
 	</section>
 	<p>%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time time={ user.last_used_at }/></b></p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			max-width 600px
@@ -314,7 +314,7 @@
 		</virtual>
 	</div>
 	<p class="empty" if={ !initializing && posts.length == 0 }>%i18n:mobile.tags.mk-user-overview-posts.no-posts%</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -370,7 +370,7 @@
 		</div>
 		<mk-time time={ post.created_at }/>
 	</a>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display inline-block
 			width 150px
@@ -443,7 +443,7 @@
 		</virtual>
 	</div>
 	<p class="empty" if={ !initializing && images.length == 0 }>%i18n:mobile.tags.mk-user-overview-photos.no-photos%</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -519,7 +519,7 @@
 				fill="#a1de41"/>
 			</g>
 	</svg>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			max-width 600px
@@ -564,7 +564,7 @@
 		</virtual>
 	</div>
 	<p class="empty" if={ user.keywords == null || user.keywords.length == 0 }>%i18n:mobile.tags.mk-user-overview-keywords.no-keywords%</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -598,7 +598,7 @@
 		</virtual>
 	</div>
 	<p class="empty" if={ user.domains == null || user.domains.length == 0 }>%i18n:mobile.tags.mk-user-overview-domains.no-domains%</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -633,7 +633,7 @@
 		</virtual>
 	</div>
 	<p class="empty" if={ !initializing && users.length == 0 }>%i18n:mobile.tags.mk-user-overview-frequently-replied-users.no-users%</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -685,7 +685,7 @@
 		</virtual>
 	</div>
 	<p class="empty" if={ !initializing && users.length == 0 }>%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/mobile/tags/users-list.tag b/src/web/app/mobile/tags/users-list.tag
index 8e18bdea3..31ca58185 100644
--- a/src/web/app/mobile/tags/users-list.tag
+++ b/src/web/app/mobile/tags/users-list.tag
@@ -11,7 +11,7 @@
 		<span if={ moreFetching }>%i18n:common.loading%<mk-ellipsis/></span></button>
 	<p class="no" if={ !fetching && users.length == 0 }>{ opts.noUsers }</p>
 	<p class="fetching" if={ fetching }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/stats/tags/index.tag b/src/web/app/stats/tags/index.tag
index 4b5415b2f..494983706 100644
--- a/src/web/app/stats/tags/index.tag
+++ b/src/web/app/stats/tags/index.tag
@@ -5,7 +5,7 @@
 		<mk-posts stats={ stats }/>
 	</main>
 	<footer><a href={ _URL_ }>{ _HOST_ }</a></footer>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0 auto
@@ -59,7 +59,7 @@
 <mk-posts>
 	<h2>%i18n:stats.posts-count% <b>{ stats.posts_count }</b></h2>
 	<mk-posts-chart if={ !initializing } data={ data }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
@@ -85,7 +85,7 @@
 <mk-users>
 	<h2>%i18n:stats.users-count% <b>{ stats.users_count }</b></h2>
 	<mk-users-chart if={ !initializing } data={ data }/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
@@ -133,7 +133,7 @@
 			stroke="#555"
 			stroke-dasharray="2 2"/>
 	</svg>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
@@ -178,7 +178,7 @@
 			stroke-width="1"
 			stroke="#555"/>
 	</svg>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 
diff --git a/src/web/app/status/tags/index.tag b/src/web/app/status/tags/index.tag
index 198aa89e3..9ac54c867 100644
--- a/src/web/app/status/tags/index.tag
+++ b/src/web/app/status/tags/index.tag
@@ -6,7 +6,7 @@
 		<mk-mem-usage connection={ connection }/>
 	</main>
 	<footer><a href={ _URL_ }>{ _HOST_ }</a></footer>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			margin 0 auto
@@ -77,7 +77,7 @@
 <mk-cpu-usage>
 	<h2>CPU <b>{ percentage }%</b></h2>
 	<mk-line-chart ref="chart"/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
@@ -107,7 +107,7 @@
 <mk-mem-usage>
 	<h2>MEM <b>{ percentage }%</b></h2>
 	<mk-line-chart ref="chart"/>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 	</style>
@@ -164,7 +164,7 @@
 			stroke="#f43b16"
 			stroke-width="0.5"/>
 	</svg>
-	<style>
+	<style lang="stylus" scoped>
 		:scope
 			display block
 			padding 16px

From fb7e9310bfbf032aecff924a4e300b2ad3aeabbb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 7 Feb 2018 18:34:43 +0900
Subject: [PATCH 008/286] wip

---
 src/web/app/auth/tags/form.tag                | 18 +++----
 src/web/app/auth/tags/index.tag               | 18 +++----
 src/web/app/ch/tags/channel.tag               | 30 +++++------
 src/web/app/ch/tags/header.tag                |  4 +-
 src/web/app/ch/tags/index.tag                 |  2 +-
 src/web/app/common/tags/activity-table.tag    |  2 +-
 src/web/app/common/tags/authorized-apps.tag   |  4 +-
 src/web/app/common/tags/error.tag             | 20 +++----
 src/web/app/common/tags/file-type-icon.tag    |  2 +-
 src/web/app/common/tags/messaging/form.tag    |  2 +-
 src/web/app/common/tags/messaging/index.tag   | 12 ++---
 src/web/app/common/tags/messaging/message.tag | 12 ++---
 src/web/app/common/tags/messaging/room.tag    | 12 ++---
 src/web/app/common/tags/poll-editor.tag       |  4 +-
 src/web/app/common/tags/poll.tag              | 10 ++--
 src/web/app/common/tags/post-menu.tag         |  4 +-
 src/web/app/common/tags/reaction-icon.tag     | 18 +++----
 ...ctions-viewer.tag => reactions-viewer.vue} | 29 ++++++----
 src/web/app/common/tags/signin-history.tag    |  6 +--
 src/web/app/common/tags/signin.tag            |  2 +-
 src/web/app/common/tags/signup.tag            | 32 +++++------
 src/web/app/common/tags/special-message.tag   |  4 +-
 src/web/app/common/tags/twitter-setting.tag   |  8 +--
 src/web/app/common/tags/uploader.tag          | 10 ++--
 .../desktop/tags/autocomplete-suggestion.tag  |  2 +-
 .../app/desktop/tags/big-follow-button.tag    | 10 ++--
 .../app/desktop/tags/detailed-post-window.tag |  2 +-
 .../app/desktop/tags/drive/browser-window.tag |  2 +-
 src/web/app/desktop/tags/drive/browser.tag    | 24 ++++-----
 src/web/app/desktop/tags/drive/file.tag       |  6 +--
 src/web/app/desktop/tags/drive/folder.tag     |  2 +-
 src/web/app/desktop/tags/drive/nav-folder.tag |  2 +-
 src/web/app/desktop/tags/follow-button.tag    | 10 ++--
 .../app/desktop/tags/following-setuper.tag    |  6 +--
 .../desktop/tags/home-widgets/access-log.tag  |  2 +-
 .../desktop/tags/home-widgets/broadcast.tag   |  8 +--
 .../app/desktop/tags/home-widgets/channel.tag | 14 ++---
 .../desktop/tags/home-widgets/mentions.tag    |  8 +--
 .../desktop/tags/home-widgets/messaging.tag   |  2 +-
 .../tags/home-widgets/notifications.tag       |  2 +-
 .../tags/home-widgets/photo-stream.tag        |  8 +--
 .../desktop/tags/home-widgets/post-form.tag   |  6 +--
 .../tags/home-widgets/recommended-polls.tag   | 12 ++---
 .../desktop/tags/home-widgets/rss-reader.tag  |  6 +--
 .../app/desktop/tags/home-widgets/server.tag  | 16 +++---
 .../desktop/tags/home-widgets/slideshow.tag   |  4 +-
 .../desktop/tags/home-widgets/timeline.tag    | 10 ++--
 .../app/desktop/tags/home-widgets/trends.tag  |  8 +--
 .../tags/home-widgets/user-recommendation.tag |  8 +--
 src/web/app/desktop/tags/home.tag             |  8 +--
 src/web/app/desktop/tags/list-user.tag        |  2 +-
 src/web/app/desktop/tags/notifications.tag    | 26 ++++-----
 src/web/app/desktop/tags/pages/entrance.tag   | 10 ++--
 .../app/desktop/tags/pages/messaging-room.tag |  2 +-
 src/web/app/desktop/tags/pages/post.tag       |  6 +--
 src/web/app/desktop/tags/post-detail-sub.tag  |  2 +-
 src/web/app/desktop/tags/post-detail.tag      | 22 ++++----
 src/web/app/desktop/tags/post-form-window.tag | 10 ++--
 src/web/app/desktop/tags/post-form.tag        |  6 +--
 src/web/app/desktop/tags/progress-dialog.tag  |  8 +--
 src/web/app/desktop/tags/repost-form.tag      |  6 +--
 src/web/app/desktop/tags/search-posts.tag     |  8 +--
 .../tags/select-file-from-drive-window.tag    |  2 +-
 src/web/app/desktop/tags/settings.tag         | 10 ++--
 src/web/app/desktop/tags/sub-post-content.tag |  8 +--
 src/web/app/desktop/tags/timeline.tag         | 34 ++++++------
 src/web/app/desktop/tags/ui.tag               | 26 ++++-----
 src/web/app/desktop/tags/user-preview.tag     |  4 +-
 src/web/app/desktop/tags/user-timeline.tag    |  8 +--
 src/web/app/desktop/tags/user.tag             | 46 ++++++++--------
 src/web/app/desktop/tags/users-list.tag       | 14 ++---
 src/web/app/desktop/tags/widgets/activity.tag |  8 +--
 src/web/app/desktop/tags/widgets/calendar.tag |  4 +-
 src/web/app/desktop/tags/window.tag           | 20 +++----
 src/web/app/dev/tags/new-app-form.tag         | 14 ++---
 src/web/app/dev/tags/pages/app.tag            |  4 +-
 src/web/app/dev/tags/pages/apps.tag           |  8 +--
 src/web/app/mobile/tags/drive-selector.tag    |  4 +-
 src/web/app/mobile/tags/drive.tag             | 36 ++++++-------
 src/web/app/mobile/tags/drive/file-viewer.tag |  6 +--
 src/web/app/mobile/tags/drive/file.tag        |  2 +-
 src/web/app/mobile/tags/follow-button.tag     | 10 ++--
 src/web/app/mobile/tags/home-timeline.tag     |  2 +-
 src/web/app/mobile/tags/init-following.tag    |  6 +--
 .../app/mobile/tags/notification-preview.tag  | 14 ++---
 src/web/app/mobile/tags/notification.tag      | 14 ++---
 src/web/app/mobile/tags/notifications.tag     | 12 ++---
 src/web/app/mobile/tags/page/entrance.tag     |  6 +--
 .../app/mobile/tags/page/messaging-room.tag   |  2 +-
 src/web/app/mobile/tags/page/post.tag         |  6 +--
 src/web/app/mobile/tags/page/selectdrive.tag  |  4 +-
 .../app/mobile/tags/page/user-followers.tag   |  2 +-
 .../app/mobile/tags/page/user-following.tag   |  2 +-
 src/web/app/mobile/tags/post-detail.tag       | 22 ++++----
 src/web/app/mobile/tags/post-form.tag         |  6 +--
 src/web/app/mobile/tags/sub-post-content.tag  |  6 +--
 src/web/app/mobile/tags/timeline.tag          | 40 +++++++-------
 src/web/app/mobile/tags/ui.tag                | 12 ++---
 src/web/app/mobile/tags/user.tag              | 54 +++++++++----------
 src/web/app/mobile/tags/users-list.tag        | 14 ++---
 src/web/app/stats/tags/index.tag              |  6 +--
 101 files changed, 534 insertions(+), 525 deletions(-)
 rename src/web/app/common/tags/{reactions-viewer.tag => reactions-viewer.vue} (58%)

diff --git a/src/web/app/auth/tags/form.tag b/src/web/app/auth/tags/form.tag
index 8f60aadb5..8c085ee9b 100644
--- a/src/web/app/auth/tags/form.tag
+++ b/src/web/app/auth/tags/form.tag
@@ -12,15 +12,15 @@
 			<h2>このアプリは次の権限を要求しています:</h2>
 			<ul>
 				<virtual each={ p in app.permission }>
-					<li if={ p == 'account-read' }>アカウントの情報を見る。</li>
-					<li if={ p == 'account-write' }>アカウントの情報を操作する。</li>
-					<li if={ p == 'post-write' }>投稿する。</li>
-					<li if={ p == 'like-write' }>いいねしたりいいね解除する。</li>
-					<li if={ p == 'following-write' }>フォローしたりフォロー解除する。</li>
-					<li if={ p == 'drive-read' }>ドライブを見る。</li>
-					<li if={ p == 'drive-write' }>ドライブを操作する。</li>
-					<li if={ p == 'notification-read' }>通知を見る。</li>
-					<li if={ p == 'notification-write' }>通知を操作する。</li>
+					<li v-if="p == 'account-read'">アカウントの情報を見る。</li>
+					<li v-if="p == 'account-write'">アカウントの情報を操作する。</li>
+					<li v-if="p == 'post-write'">投稿する。</li>
+					<li v-if="p == 'like-write'">いいねしたりいいね解除する。</li>
+					<li v-if="p == 'following-write'">フォローしたりフォロー解除する。</li>
+					<li v-if="p == 'drive-read'">ドライブを見る。</li>
+					<li v-if="p == 'drive-write'">ドライブを操作する。</li>
+					<li v-if="p == 'notification-read'">通知を見る。</li>
+					<li v-if="p == 'notification-write'">通知を操作する。</li>
 				</virtual>
 			</ul>
 		</section>
diff --git a/src/web/app/auth/tags/index.tag b/src/web/app/auth/tags/index.tag
index e1c0cb82e..195c66909 100644
--- a/src/web/app/auth/tags/index.tag
+++ b/src/web/app/auth/tags/index.tag
@@ -1,21 +1,21 @@
 <mk-index>
-	<main if={ SIGNIN }>
-		<p class="fetching" if={ fetching }>読み込み中<mk-ellipsis/></p>
-		<mk-form ref="form" if={ state == 'waiting' } session={ session }/>
-		<div class="denied" if={ state == 'denied' }>
+	<main v-if="SIGNIN">
+		<p class="fetching" v-if="fetching">読み込み中<mk-ellipsis/></p>
+		<mk-form ref="form" v-if="state == 'waiting'" session={ session }/>
+		<div class="denied" v-if="state == 'denied'">
 			<h1>アプリケーションの連携をキャンセルしました。</h1>
 			<p>このアプリがあなたのアカウントにアクセスすることはありません。</p>
 		</div>
-		<div class="accepted" if={ state == 'accepted' }>
+		<div class="accepted" v-if="state == 'accepted'">
 			<h1>{ session.app.is_authorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました'}</h1>
-			<p if={ session.app.callback_url }>アプリケーションに戻っています<mk-ellipsis/></p>
-			<p if={ !session.app.callback_url }>アプリケーションに戻って、やっていってください。</p>
+			<p v-if="session.app.callback_url">アプリケーションに戻っています<mk-ellipsis/></p>
+			<p v-if="!session.app.callback_url">アプリケーションに戻って、やっていってください。</p>
 		</div>
-		<div class="error" if={ state == 'fetch-session-error' }>
+		<div class="error" v-if="state == 'fetch-session-error'">
 			<p>セッションが存在しません。</p>
 		</div>
 	</main>
-	<main class="signin" if={ !SIGNIN }>
+	<main class="signin" v-if="!SIGNIN">
 		<h1>サインインしてください</h1>
 		<mk-signin/>
 	</main>
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index ea0234340..b01c2b548 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -1,12 +1,12 @@
 <mk-channel>
 	<mk-header/>
 	<hr>
-	<main if={ !fetching }>
+	<main v-if="!fetching">
 		<h1>{ channel.title }</h1>
 
-		<div if={ SIGNIN }>
-			<p if={ channel.is_watching }>このチャンネルをウォッチしています <a @click="unwatch">ウォッチ解除</a></p>
-			<p if={ !channel.is_watching }><a @click="watch">このチャンネルをウォッチする</a></p>
+		<div v-if="SIGNIN">
+			<p v-if="channel.is_watching">このチャンネルをウォッチしています <a @click="unwatch">ウォッチ解除</a></p>
+			<p v-if="!channel.is_watching"><a @click="watch">このチャンネルをウォッチする</a></p>
 		</div>
 
 		<div class="share">
@@ -15,17 +15,17 @@
 		</div>
 
 		<div class="body">
-			<p if={ postsFetching }>読み込み中<mk-ellipsis/></p>
-			<div if={ !postsFetching }>
-				<p if={ posts == null || posts.length == 0 }>まだ投稿がありません</p>
-				<virtual if={ posts != null }>
+			<p v-if="postsFetching">読み込み中<mk-ellipsis/></p>
+			<div v-if="!postsFetching">
+				<p v-if="posts == null || posts.length == 0">まだ投稿がありません</p>
+				<virtual v-if="posts != null">
 					<mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/>
 				</virtual>
 			</div>
 		</div>
 		<hr>
-		<mk-channel-form if={ SIGNIN } channel={ channel } ref="form"/>
-		<div if={ !SIGNIN }>
+		<mk-channel-form v-if="SIGNIN" channel={ channel } ref="form"/>
+		<div v-if="!SIGNIN">
 			<p>参加するには<a href={ _URL_ }>ログインまたは新規登録</a>してください</p>
 		</div>
 		<hr>
@@ -171,9 +171,9 @@
 		<span>ID:<i>{ post.user.username }</i></span>
 	</header>
 	<div>
-		<a if={ post.reply }>&gt;&gt;{ post.reply.index }</a>
+		<a v-if="post.reply">&gt;&gt;{ post.reply.index }</a>
 		{ post.text }
-		<div class="media" if={ post.media }>
+		<div class="media" v-if="post.media">
 			<virtual each={ file in post.media }>
 				<a href={ file.url } target="_blank">
 					<img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
@@ -241,17 +241,17 @@
 </mk-channel-post>
 
 <mk-channel-form>
-	<p if={ reply }><b>&gt;&gt;{ reply.index }</b> ({ reply.user.name }): <a @click="clearReply">[x]</a></p>
+	<p v-if="reply"><b>&gt;&gt;{ reply.index }</b> ({ reply.user.name }): <a @click="clearReply">[x]</a></p>
 	<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder="%i18n:ch.tags.mk-channel-form.textarea%"></textarea>
 	<div class="actions">
 		<button @click="selectFile">%fa:upload%%i18n:ch.tags.mk-channel-form.upload%</button>
 		<button @click="drive">%fa:cloud%%i18n:ch.tags.mk-channel-form.drive%</button>
 		<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } @click="post">
-			<virtual if={ !wait }>%fa:paper-plane%</virtual>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.post%' }<mk-ellipsis if={ wait }/>
+			<virtual v-if="!wait">%fa:paper-plane%</virtual>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.post%' }<mk-ellipsis v-if="wait"/>
 		</button>
 	</div>
 	<mk-uploader ref="uploader"/>
-	<ol if={ files }>
+	<ol v-if="files">
 		<li each={ files }>{ name }</li>
 	</ol>
 	<input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
diff --git a/src/web/app/ch/tags/header.tag b/src/web/app/ch/tags/header.tag
index 8af6f1c37..84575b03d 100644
--- a/src/web/app/ch/tags/header.tag
+++ b/src/web/app/ch/tags/header.tag
@@ -3,8 +3,8 @@
 		<a href={ _CH_URL_ }>Index</a> | <a href={ _URL_ }>Misskey</a>
 	</div>
 	<div>
-		<a if={ !SIGNIN } href={ _URL_ }>ログイン(新規登録)</a>
-		<a if={ SIGNIN } href={ _URL_ + '/' + I.username }>{ I.username }</a>
+		<a v-if="!SIGNIN" href={ _URL_ }>ログイン(新規登録)</a>
+		<a v-if="SIGNIN" href={ _URL_ + '/' + I.username }>{ I.username }</a>
 	</div>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/ch/tags/index.tag b/src/web/app/ch/tags/index.tag
index 2fd549368..e058da6a3 100644
--- a/src/web/app/ch/tags/index.tag
+++ b/src/web/app/ch/tags/index.tag
@@ -3,7 +3,7 @@
 	<hr>
 	<button @click="n">%i18n:ch.tags.mk-index.new%</button>
 	<hr>
-	<ul if={ channels }>
+	<ul v-if="channels">
 		<li each={ channels }><a href={ '/' + this.id }>{ this.title }</a></li>
 	</ul>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/common/tags/activity-table.tag b/src/web/app/common/tags/activity-table.tag
index b0a100090..39d4d7205 100644
--- a/src/web/app/common/tags/activity-table.tag
+++ b/src/web/app/common/tags/activity-table.tag
@@ -1,5 +1,5 @@
 <mk-activity-table>
-	<svg if={ data } ref="canvas" viewBox="0 0 53 7" preserveAspectRatio="none">
+	<svg v-if="data" ref="canvas" viewBox="0 0 53 7" preserveAspectRatio="none">
 		<rect each={ data } width="1" height="1"
 			riot-x={ x } riot-y={ date.weekday }
 			rx="1" ry="1"
diff --git a/src/web/app/common/tags/authorized-apps.tag b/src/web/app/common/tags/authorized-apps.tag
index 324871949..0511c1bc6 100644
--- a/src/web/app/common/tags/authorized-apps.tag
+++ b/src/web/app/common/tags/authorized-apps.tag
@@ -1,8 +1,8 @@
 <mk-authorized-apps>
-	<div class="none ui info" if={ !fetching && apps.length == 0 }>
+	<div class="none ui info" v-if="!fetching && apps.length == 0">
 		<p>%fa:info-circle%%i18n:common.tags.mk-authorized-apps.no-apps%</p>
 	</div>
-	<div class="apps" if={ apps.length != 0 }>
+	<div class="apps" v-if="apps.length != 0">
 		<div each={ app in apps }>
 			<p><b>{ app.name }</b></p>
 			<p>{ app.description }</p>
diff --git a/src/web/app/common/tags/error.tag b/src/web/app/common/tags/error.tag
index 0a6535762..f72f403a9 100644
--- a/src/web/app/common/tags/error.tag
+++ b/src/web/app/common/tags/error.tag
@@ -8,8 +8,8 @@
 	}</a>{
 		'%i18n:common.tags.mk-error.description%'.substr('%i18n:common.tags.mk-error.description%'.indexOf('}') + 1)
 	}</p>
-	<button if={ !troubleshooting } @click="troubleshoot">%i18n:common.tags.mk-error.troubleshoot%</button>
-	<mk-troubleshooter if={ troubleshooting }/>
+	<button v-if="!troubleshooting" @click="troubleshoot">%i18n:common.tags.mk-error.troubleshoot%</button>
+	<mk-troubleshooter v-if="troubleshooting"/>
 	<p class="thanks">%i18n:common.tags.mk-error.thanks%</p>
 	<style lang="stylus" scoped>
 		:scope
@@ -98,15 +98,15 @@
 <mk-troubleshooter>
 	<h1>%fa:wrench%%i18n:common.tags.mk-error.troubleshooter.title%</h1>
 	<div>
-		<p data-wip={ network == null }><virtual if={ network != null }><virtual if={ network }>%fa:check%</virtual><virtual if={ !network }>%fa:times%</virtual></virtual>{ network == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-network%' : '%i18n:common.tags.mk-error.troubleshooter.network%' }<mk-ellipsis if={ network == null }/></p>
-		<p if={ network == true } data-wip={ internet == null }><virtual if={ internet != null }><virtual if={ internet }>%fa:check%</virtual><virtual if={ !internet }>%fa:times%</virtual></virtual>{ internet == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-internet%' : '%i18n:common.tags.mk-error.troubleshooter.internet%' }<mk-ellipsis if={ internet == null }/></p>
-		<p if={ internet == true } data-wip={ server == null }><virtual if={ server != null }><virtual if={ server }>%fa:check%</virtual><virtual if={ !server }>%fa:times%</virtual></virtual>{ server == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-server%' : '%i18n:common.tags.mk-error.troubleshooter.server%' }<mk-ellipsis if={ server == null }/></p>
+		<p data-wip={ network == null }><virtual v-if="network != null"><virtual v-if="network">%fa:check%</virtual><virtual v-if="!network">%fa:times%</virtual></virtual>{ network == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-network%' : '%i18n:common.tags.mk-error.troubleshooter.network%' }<mk-ellipsis v-if="network == null"/></p>
+		<p v-if="network == true" data-wip={ internet == null }><virtual v-if="internet != null"><virtual v-if="internet">%fa:check%</virtual><virtual v-if="!internet">%fa:times%</virtual></virtual>{ internet == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-internet%' : '%i18n:common.tags.mk-error.troubleshooter.internet%' }<mk-ellipsis v-if="internet == null"/></p>
+		<p v-if="internet == true" data-wip={ server == null }><virtual v-if="server != null"><virtual v-if="server">%fa:check%</virtual><virtual v-if="!server">%fa:times%</virtual></virtual>{ server == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-server%' : '%i18n:common.tags.mk-error.troubleshooter.server%' }<mk-ellipsis v-if="server == null"/></p>
 	</div>
-	<p if={ !end }>%i18n:common.tags.mk-error.troubleshooter.finding%<mk-ellipsis/></p>
-	<p if={ network === false }><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-network%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-network-desc%</p>
-	<p if={ internet === false }><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-internet%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-internet-desc%</p>
-	<p if={ server === false }><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-server%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-server-desc%</p>
-	<p if={ server === true } class="success"><b>%fa:info-circle%%i18n:common.tags.mk-error.troubleshooter.success%</b><br>%i18n:common.tags.mk-error.troubleshooter.success-desc%</p>
+	<p v-if="!end">%i18n:common.tags.mk-error.troubleshooter.finding%<mk-ellipsis/></p>
+	<p v-if="network === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-network%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-network-desc%</p>
+	<p v-if="internet === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-internet%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-internet-desc%</p>
+	<p v-if="server === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-server%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-server-desc%</p>
+	<p v-if="server === true" class="success"><b>%fa:info-circle%%i18n:common.tags.mk-error.troubleshooter.success%</b><br>%i18n:common.tags.mk-error.troubleshooter.success-desc%</p>
 
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/common/tags/file-type-icon.tag b/src/web/app/common/tags/file-type-icon.tag
index 035aec247..d47f96fd0 100644
--- a/src/web/app/common/tags/file-type-icon.tag
+++ b/src/web/app/common/tags/file-type-icon.tag
@@ -1,5 +1,5 @@
 <mk-file-type-icon>
-	<virtual if={ kind == 'image' }>%fa:file-image%</virtual>
+	<virtual v-if="kind == 'image'">%fa:file-image%</virtual>
 	<style lang="stylus" scoped>
 		:scope
 			display inline
diff --git a/src/web/app/common/tags/messaging/form.tag b/src/web/app/common/tags/messaging/form.tag
index 33b0beb88..df0658741 100644
--- a/src/web/app/common/tags/messaging/form.tag
+++ b/src/web/app/common/tags/messaging/form.tag
@@ -3,7 +3,7 @@
 	<div class="files"></div>
 	<mk-uploader ref="uploader"/>
 	<button class="send" @click="send" disabled={ sending } title="%i18n:common.send%">
-		<virtual if={ !sending }>%fa:paper-plane%</virtual><virtual if={ sending }>%fa:spinner .spin%</virtual>
+		<virtual v-if="!sending">%fa:paper-plane%</virtual><virtual v-if="sending">%fa:spinner .spin%</virtual>
 	</button>
 	<button class="attach-from-local" type="button" title="%i18n:common.tags.mk-messaging-form.attach-from-local%">
 		%fa:upload%
diff --git a/src/web/app/common/tags/messaging/index.tag b/src/web/app/common/tags/messaging/index.tag
index 24d49257c..fa12a78d8 100644
--- a/src/web/app/common/tags/messaging/index.tag
+++ b/src/web/app/common/tags/messaging/index.tag
@@ -1,11 +1,11 @@
 <mk-messaging data-compact={ opts.compact }>
-	<div class="search" if={ !opts.compact }>
+	<div class="search" v-if="!opts.compact">
 		<div class="form">
 			<label for="search-input">%fa:search%</label>
 			<input ref="search" type="search" oninput={ search } onkeydown={ onSearchKeydown } placeholder="%i18n:common.tags.mk-messaging.search-user%"/>
 		</div>
 		<div class="result">
-			<ol class="users" if={ searchResult.length > 0 } ref="searchResult">
+			<ol class="users" v-if="searchResult.length > 0" ref="searchResult">
 				<li each={ user, i in searchResult } onkeydown={ parent.onSearchResultKeydown.bind(null, i) } @click="user._click" tabindex="-1">
 					<img class="avatar" src={ user.avatar_url + '?thumbnail&size=32' } alt=""/>
 					<span class="name">{ user.name }</span>
@@ -14,7 +14,7 @@
 			</ol>
 		</div>
 	</div>
-	<div class="history" if={ history.length > 0 }>
+	<div class="history" v-if="history.length > 0">
 		<virtual each={ history }>
 			<a class="user" data-is-me={ is_me } data-is-read={ is_read } @click="_click">
 				<div>
@@ -25,14 +25,14 @@
 						<mk-time time={ created_at }/>
 					</header>
 					<div class="body">
-						<p class="text"><span class="me" if={ is_me }>%i18n:common.tags.mk-messaging.you%:</span>{ text }</p>
+						<p class="text"><span class="me" v-if="is_me">%i18n:common.tags.mk-messaging.you%:</span>{ text }</p>
 					</div>
 				</div>
 			</a>
 		</virtual>
 	</div>
-	<p class="no-history" if={ !fetching && history.length == 0 }>%i18n:common.tags.mk-messaging.no-history%</p>
-	<p class="fetching" if={ fetching }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<p class="no-history" v-if="!fetching && history.length == 0">%i18n:common.tags.mk-messaging.no-history%</p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/common/tags/messaging/message.tag b/src/web/app/common/tags/messaging/message.tag
index d65bb8770..4f75e9049 100644
--- a/src/web/app/common/tags/messaging/message.tag
+++ b/src/web/app/common/tags/messaging/message.tag
@@ -4,18 +4,18 @@
 	</a>
 	<div class="content-container">
 		<div class="balloon">
-			<p class="read" if={ message.is_me && message.is_read }>%i18n:common.tags.mk-messaging-message.is-read%</p>
-			<button class="delete-button" if={ message.is_me } title="%i18n:common.delete%"><img src="/assets/desktop/messaging/delete.png" alt="Delete"/></button>
-			<div class="content" if={ !message.is_deleted }>
+			<p class="read" v-if="message.is_me && message.is_read">%i18n:common.tags.mk-messaging-message.is-read%</p>
+			<button class="delete-button" v-if="message.is_me" title="%i18n:common.delete%"><img src="/assets/desktop/messaging/delete.png" alt="Delete"/></button>
+			<div class="content" v-if="!message.is_deleted">
 				<div ref="text"></div>
-				<div class="image" if={ message.file }><img src={ message.file.url } alt="image" title={ message.file.name }/></div>
+				<div class="image" v-if="message.file"><img src={ message.file.url } alt="image" title={ message.file.name }/></div>
 			</div>
-			<div class="content" if={ message.is_deleted }>
+			<div class="content" v-if="message.is_deleted">
 				<p class="is-deleted">%i18n:common.tags.mk-messaging-message.deleted%</p>
 			</div>
 		</div>
 		<footer>
-			<mk-time time={ message.created_at }/><virtual if={ message.is_edited }>%fa:pencil-alt%</virtual>
+			<mk-time time={ message.created_at }/><virtual v-if="message.is_edited">%fa:pencil-alt%</virtual>
 		</footer>
 	</div>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/common/tags/messaging/room.tag b/src/web/app/common/tags/messaging/room.tag
index 0a669dbc9..e659b778b 100644
--- a/src/web/app/common/tags/messaging/room.tag
+++ b/src/web/app/common/tags/messaging/room.tag
@@ -1,14 +1,14 @@
 <mk-messaging-room>
 	<div class="stream">
-		<p class="init" if={ init }>%fa:spinner .spin%%i18n:common.loading%</p>
-		<p class="empty" if={ !init && messages.length == 0 }>%fa:info-circle%%i18n:common.tags.mk-messaging-room.empty%</p>
-		<p class="no-history" if={ !init && messages.length > 0 && !moreMessagesIsInStock }>%fa:flag%%i18n:common.tags.mk-messaging-room.no-history%</p>
-		<button class="more { fetching: fetchingMoreMessages }" if={ moreMessagesIsInStock } @click="fetchMoreMessages" disabled={ fetchingMoreMessages }>
-			<virtual if={ fetchingMoreMessages }>%fa:spinner .pulse .fw%</virtual>{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' }
+		<p class="init" v-if="init">%fa:spinner .spin%%i18n:common.loading%</p>
+		<p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:common.tags.mk-messaging-room.empty%</p>
+		<p class="no-history" v-if="!init && messages.length > 0 && !moreMessagesIsInStock">%fa:flag%%i18n:common.tags.mk-messaging-room.no-history%</p>
+		<button class="more { fetching: fetchingMoreMessages }" v-if="moreMessagesIsInStock" @click="fetchMoreMessages" disabled={ fetchingMoreMessages }>
+			<virtual v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</virtual>{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' }
 		</button>
 		<virtual each={ message, i in messages }>
 			<mk-messaging-message message={ message }/>
-			<p class="date" if={ i != messages.length - 1 && message._date != messages[i + 1]._date }><span>{ messages[i + 1]._datetext }</span></p>
+			<p class="date" v-if="i != messages.length - 1 && message._date != messages[i + 1]._date"><span>{ messages[i + 1]._datetext }</span></p>
 		</virtual>
 	</div>
 	<footer>
diff --git a/src/web/app/common/tags/poll-editor.tag b/src/web/app/common/tags/poll-editor.tag
index 28e059e87..1d57eb9de 100644
--- a/src/web/app/common/tags/poll-editor.tag
+++ b/src/web/app/common/tags/poll-editor.tag
@@ -1,5 +1,5 @@
 <mk-poll-editor>
-	<p class="caution" if={ choices.length < 2 }>
+	<p class="caution" v-if="choices.length < 2">
 		%fa:exclamation-triangle%%i18n:common.tags.mk-poll-editor.no-only-one-choice%
 	</p>
 	<ul ref="choices">
@@ -10,7 +10,7 @@
 			</button>
 		</li>
 	</ul>
-	<button class="add" if={ choices.length < 10 } @click="add">%i18n:common.tags.mk-poll-editor.add%</button>
+	<button class="add" v-if="choices.length < 10" @click="add">%i18n:common.tags.mk-poll-editor.add%</button>
 	<button class="destroy" @click="destroy" title="%i18n:common.tags.mk-poll-editor.destroy%">
 		%fa:times%
 	</button>
diff --git a/src/web/app/common/tags/poll.tag b/src/web/app/common/tags/poll.tag
index 003368815..e6971d5bb 100644
--- a/src/web/app/common/tags/poll.tag
+++ b/src/web/app/common/tags/poll.tag
@@ -3,17 +3,17 @@
 		<li each={ poll.choices } @click="vote.bind(null, id)" class={ voted: voted } title={ !parent.isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', text) : '' }>
 			<div class="backdrop" style={ 'width:' + (parent.result ? (votes / parent.total * 100) : 0) + '%' }></div>
 			<span>
-				<virtual if={ is_voted }>%fa:check%</virtual>
+				<virtual v-if="is_voted">%fa:check%</virtual>
 				{ text }
-				<span class="votes" if={ parent.result }>({ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', votes) })</span>
+				<span class="votes" v-if="parent.result">({ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', votes) })</span>
 			</span>
 		</li>
 	</ul>
-	<p if={ total > 0 }>
+	<p v-if="total > 0">
 		<span>{ '%i18n:common.tags.mk-poll.total-users%'.replace('{}', total) }</span>
 		・
-		<a if={ !isVoted } @click="toggleResult">{ result ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }</a>
-		<span if={ isVoted }>%i18n:common.tags.mk-poll.voted%</span>
+		<a v-if="!isVoted" @click="toggleResult">{ result ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }</a>
+		<span v-if="isVoted">%i18n:common.tags.mk-poll.voted%</span>
 	</p>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/common/tags/post-menu.tag b/src/web/app/common/tags/post-menu.tag
index da5eaf8ed..f3b13c0b1 100644
--- a/src/web/app/common/tags/post-menu.tag
+++ b/src/web/app/common/tags/post-menu.tag
@@ -1,8 +1,8 @@
 <mk-post-menu>
 	<div class="backdrop" ref="backdrop" @click="close"></div>
 	<div class="popover { compact: opts.compact }" ref="popover">
-		<button if={ post.user_id === I.id } @click="pin">%i18n:common.tags.mk-post-menu.pin%</button>
-		<div if={ I.is_pro && !post.is_category_verified }>
+		<button v-if="post.user_id === I.id" @click="pin">%i18n:common.tags.mk-post-menu.pin%</button>
+		<div v-if="I.is_pro && !post.is_category_verified">
 			<select ref="categorySelect">
 				<option value="">%i18n:common.tags.mk-post-menu.select%</option>
 				<option value="music">%i18n:common.post_categories.music%</option>
diff --git a/src/web/app/common/tags/reaction-icon.tag b/src/web/app/common/tags/reaction-icon.tag
index 50d62cfba..2282a5868 100644
--- a/src/web/app/common/tags/reaction-icon.tag
+++ b/src/web/app/common/tags/reaction-icon.tag
@@ -1,13 +1,13 @@
 <mk-reaction-icon>
-	<virtual if={ opts.reaction == 'like' }><img src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%"></virtual>
-	<virtual if={ opts.reaction == 'love' }><img src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%"></virtual>
-	<virtual if={ opts.reaction == 'laugh' }><img src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%"></virtual>
-	<virtual if={ opts.reaction == 'hmm' }><img src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%"></virtual>
-	<virtual if={ opts.reaction == 'surprise' }><img src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%"></virtual>
-	<virtual if={ opts.reaction == 'congrats' }><img src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%"></virtual>
-	<virtual if={ opts.reaction == 'angry' }><img src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%"></virtual>
-	<virtual if={ opts.reaction == 'confused' }><img src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%"></virtual>
-	<virtual if={ opts.reaction == 'pudding' }><img src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%"></virtual>
+	<virtual v-if="opts.reaction == 'like'"><img src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%"></virtual>
+	<virtual v-if="opts.reaction == 'love'"><img src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%"></virtual>
+	<virtual v-if="opts.reaction == 'laugh'"><img src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%"></virtual>
+	<virtual v-if="opts.reaction == 'hmm'"><img src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%"></virtual>
+	<virtual v-if="opts.reaction == 'surprise'"><img src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%"></virtual>
+	<virtual v-if="opts.reaction == 'congrats'"><img src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%"></virtual>
+	<virtual v-if="opts.reaction == 'angry'"><img src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%"></virtual>
+	<virtual v-if="opts.reaction == 'confused'"><img src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%"></virtual>
+	<virtual v-if="opts.reaction == 'pudding'"><img src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%"></virtual>
 
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/common/tags/reactions-viewer.tag b/src/web/app/common/tags/reactions-viewer.vue
similarity index 58%
rename from src/web/app/common/tags/reactions-viewer.tag
rename to src/web/app/common/tags/reactions-viewer.vue
index 8ec14a12f..ad126ff1d 100644
--- a/src/web/app/common/tags/reactions-viewer.tag
+++ b/src/web/app/common/tags/reactions-viewer.vue
@@ -1,14 +1,23 @@
+<template>
+<div>
+	<template v-if="reactions">
+		<span v-if="reactions.like"><mk-reaction-icon reaction='like'/><span>{ reactions.like }</span></span>
+		<span v-if="reactions.love"><mk-reaction-icon reaction='love'/><span>{ reactions.love }</span></span>
+		<span v-if="reactions.laugh"><mk-reaction-icon reaction='laugh'/><span>{ reactions.laugh }</span></span>
+		<span v-if="reactions.hmm"><mk-reaction-icon reaction='hmm'/><span>{ reactions.hmm }</span></span>
+		<span v-if="reactions.surprise"><mk-reaction-icon reaction='surprise'/><span>{ reactions.surprise }</span></span>
+		<span v-if="reactions.congrats"><mk-reaction-icon reaction='congrats'/><span>{ reactions.congrats }</span></span>
+		<span v-if="reactions.angry"><mk-reaction-icon reaction='angry'/><span>{ reactions.angry }</span></span>
+		<span v-if="reactions.confused"><mk-reaction-icon reaction='confused'/><span>{ reactions.confused }</span></span>
+		<span v-if="reactions.pudding"><mk-reaction-icon reaction='pudding'/><span>{ reactions.pudding }</span></span>
+	</template>
+</div>
+</template>
+
+
 <mk-reactions-viewer>
-	<virtual if={ reactions }>
-		<span if={ reactions.like }><mk-reaction-icon reaction='like'/><span>{ reactions.like }</span></span>
-		<span if={ reactions.love }><mk-reaction-icon reaction='love'/><span>{ reactions.love }</span></span>
-		<span if={ reactions.laugh }><mk-reaction-icon reaction='laugh'/><span>{ reactions.laugh }</span></span>
-		<span if={ reactions.hmm }><mk-reaction-icon reaction='hmm'/><span>{ reactions.hmm }</span></span>
-		<span if={ reactions.surprise }><mk-reaction-icon reaction='surprise'/><span>{ reactions.surprise }</span></span>
-		<span if={ reactions.congrats }><mk-reaction-icon reaction='congrats'/><span>{ reactions.congrats }</span></span>
-		<span if={ reactions.angry }><mk-reaction-icon reaction='angry'/><span>{ reactions.angry }</span></span>
-		<span if={ reactions.confused }><mk-reaction-icon reaction='confused'/><span>{ reactions.confused }</span></span>
-		<span if={ reactions.pudding }><mk-reaction-icon reaction='pudding'/><span>{ reactions.pudding }</span></span>
+	<virtual v-if="reactions">
+		
 	</virtual>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/common/tags/signin-history.tag b/src/web/app/common/tags/signin-history.tag
index 332bfdccf..e6b57c091 100644
--- a/src/web/app/common/tags/signin-history.tag
+++ b/src/web/app/common/tags/signin-history.tag
@@ -1,5 +1,5 @@
 <mk-signin-history>
-	<div class="records" if={ history.length != 0 }>
+	<div class="records" v-if="history.length != 0">
 		<mk-signin-record each={ rec in history } rec={ rec }/>
 	</div>
 	<style lang="stylus" scoped>
@@ -43,8 +43,8 @@
 
 <mk-signin-record>
 	<header @click="toggle">
-		<virtual if={ rec.success }>%fa:check%</virtual>
-		<virtual if={ !rec.success }>%fa:times%</virtual>
+		<virtual v-if="rec.success">%fa:check%</virtual>
+		<virtual v-if="!rec.success">%fa:times%</virtual>
 		<span class="ip">{ rec.ip }</span>
 		<mk-time time={ rec.created_at }/>
 	</header>
diff --git a/src/web/app/common/tags/signin.tag b/src/web/app/common/tags/signin.tag
index 949217c71..3fa253fbb 100644
--- a/src/web/app/common/tags/signin.tag
+++ b/src/web/app/common/tags/signin.tag
@@ -6,7 +6,7 @@
 		<label class="password">
 			<input ref="password" type="password" placeholder="%i18n:common.tags.mk-signin.password%" required="required"/>%fa:lock%
 		</label>
-		<label class="token" if={ user && user.two_factor_enabled }>
+		<label class="token" v-if="user && user.two_factor_enabled">
 			<input ref="token" type="number" placeholder="%i18n:common.tags.mk-signin.token%" required="required"/>%fa:lock%
 		</label>
 		<button type="submit" disabled={ signing }>{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }</button>
diff --git a/src/web/app/common/tags/signup.tag b/src/web/app/common/tags/signup.tag
index 861c65201..1efb4aa09 100644
--- a/src/web/app/common/tags/signup.tag
+++ b/src/web/app/common/tags/signup.tag
@@ -3,34 +3,34 @@
 		<label class="username">
 			<p class="caption">%fa:at%%i18n:common.tags.mk-signup.username%</p>
 			<input ref="username" type="text" pattern="^[a-zA-Z0-9-]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required="required" onkeyup={ onChangeUsername }/>
-			<p class="profile-page-url-preview" if={ refs.username.value != '' && username-state != 'invalidFormat' && username-state != 'minRange' && username-state != 'maxRange' }>{ _URL_ + '/' + refs.username.value }</p>
-			<p class="info" if={ usernameState == 'wait' } style="color:#999">%fa:spinner .pulse .fw%%i18n:common.tags.mk-signup.checking%</p>
-			<p class="info" if={ usernameState == 'ok' } style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.available%</p>
-			<p class="info" if={ usernameState == 'unavailable' } style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.unavailable%</p>
-			<p class="info" if={ usernameState == 'error' } style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.error%</p>
-			<p class="info" if={ usernameState == 'invalid-format' } style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.invalid-format%</p>
-			<p class="info" if={ usernameState == 'min-range' } style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-short%</p>
-			<p class="info" if={ usernameState == 'max-range' } style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-long%</p>
+			<p class="profile-page-url-preview" v-if="refs.username.value != '' && username-state != 'invalidFormat' && username-state != 'minRange' && username-state != 'maxRange'">{ _URL_ + '/' + refs.username.value }</p>
+			<p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:common.tags.mk-signup.checking%</p>
+			<p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.available%</p>
+			<p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.unavailable%</p>
+			<p class="info" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.error%</p>
+			<p class="info" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.invalid-format%</p>
+			<p class="info" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-short%</p>
+			<p class="info" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-long%</p>
 		</label>
 		<label class="password">
 			<p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%</p>
 			<input ref="password" type="password" placeholder="%i18n:common.tags.mk-signup.password-placeholder%" autocomplete="off" required="required" onkeyup={ onChangePassword }/>
-			<div class="meter" if={ passwordStrength != '' } data-strength={ passwordStrength }>
+			<div class="meter" v-if="passwordStrength != ''" data-strength={ passwordStrength }>
 				<div class="value" ref="passwordMetar"></div>
 			</div>
-			<p class="info" if={ passwordStrength == 'low' } style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.weak-password%</p>
-			<p class="info" if={ passwordStrength == 'medium' } style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.normal-password%</p>
-			<p class="info" if={ passwordStrength == 'high' } style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.strong-password%</p>
+			<p class="info" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.weak-password%</p>
+			<p class="info" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.normal-password%</p>
+			<p class="info" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.strong-password%</p>
 		</label>
 		<label class="retype-password">
 			<p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%(%i18n:common.tags.mk-signup.retype%)</p>
 			<input ref="passwordRetype" type="password" placeholder="%i18n:common.tags.mk-signup.retype-placeholder%" autocomplete="off" required="required" onkeyup={ onChangePasswordRetype }/>
-			<p class="info" if={ passwordRetypeState == 'match' } style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.password-matched%</p>
-			<p class="info" if={ passwordRetypeState == 'not-match' } style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.password-not-matched%</p>
+			<p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.password-matched%</p>
+			<p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.password-not-matched%</p>
 		</label>
 		<label class="recaptcha">
-			<p class="caption"><virtual if={ recaptchaed }>%fa:toggle-on%</virtual><virtual if={ !recaptchaed }>%fa:toggle-off%</virtual>%i18n:common.tags.mk-signup.recaptcha%</p>
-			<div if={ recaptcha } class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" data-sitekey={ recaptcha.site_key }></div>
+			<p class="caption"><virtual v-if="recaptchaed">%fa:toggle-on%</virtual><virtual v-if="!recaptchaed">%fa:toggle-off%</virtual>%i18n:common.tags.mk-signup.recaptcha%</p>
+			<div v-if="recaptcha" class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" data-sitekey={ recaptcha.site_key }></div>
 		</label>
 		<label class="agree-tou">
 			<input name="agree-tou" type="checkbox" autocomplete="off" required="required"/>
diff --git a/src/web/app/common/tags/special-message.tag b/src/web/app/common/tags/special-message.tag
index 5d62797ae..24fe66652 100644
--- a/src/web/app/common/tags/special-message.tag
+++ b/src/web/app/common/tags/special-message.tag
@@ -1,6 +1,6 @@
 <mk-special-message>
-	<p if={ m == 1 && d == 1 }>%i18n:common.tags.mk-special-message.new-year%</p>
-	<p if={ m == 12 && d == 25 }>%i18n:common.tags.mk-special-message.christmas%</p>
+	<p v-if="m == 1 && d == 1">%i18n:common.tags.mk-special-message.new-year%</p>
+	<p v-if="m == 12 && d == 25">%i18n:common.tags.mk-special-message.christmas%</p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/common/tags/twitter-setting.tag b/src/web/app/common/tags/twitter-setting.tag
index f865de466..cb3d1e56a 100644
--- a/src/web/app/common/tags/twitter-setting.tag
+++ b/src/web/app/common/tags/twitter-setting.tag
@@ -1,12 +1,12 @@
 <mk-twitter-setting>
 	<p>%i18n:common.tags.mk-twitter-setting.description%<a href={ _DOCS_URL_ + '/link-to-twitter' } target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p>
-	<p class="account" if={ I.twitter } title={ 'Twitter ID: ' + I.twitter.user_id }>%i18n:common.tags.mk-twitter-setting.connected-to%: <a href={ 'https://twitter.com/' + I.twitter.screen_name } target="_blank">@{ I.twitter.screen_name }</a></p>
+	<p class="account" v-if="I.twitter" title={ 'Twitter ID: ' + I.twitter.user_id }>%i18n:common.tags.mk-twitter-setting.connected-to%: <a href={ 'https://twitter.com/' + I.twitter.screen_name } target="_blank">@{ I.twitter.screen_name }</a></p>
 	<p>
 		<a href={ _API_URL_ + '/connect/twitter' } target="_blank" @click="connect">{ I.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }</a>
-		<span if={ I.twitter }> or </span>
-		<a href={ _API_URL_ + '/disconnect/twitter' } target="_blank" if={ I.twitter } @click="disconnect">%i18n:common.tags.mk-twitter-setting.disconnect%</a>
+		<span v-if="I.twitter"> or </span>
+		<a href={ _API_URL_ + '/disconnect/twitter' } target="_blank" v-if="I.twitter" @click="disconnect">%i18n:common.tags.mk-twitter-setting.disconnect%</a>
 	</p>
-	<p class="id" if={ I.twitter }>Twitter ID: { I.twitter.user_id }</p>
+	<p class="id" v-if="I.twitter">Twitter ID: { I.twitter.user_id }</p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/common/tags/uploader.tag b/src/web/app/common/tags/uploader.tag
index ec9ba0243..cc555304d 100644
--- a/src/web/app/common/tags/uploader.tag
+++ b/src/web/app/common/tags/uploader.tag
@@ -1,12 +1,12 @@
 <mk-uploader>
-	<ol if={ uploads.length > 0 }>
+	<ol v-if="uploads.length > 0">
 		<li each={ uploads }>
 			<div class="img" style="background-image: url({ img })"></div>
 			<p class="name">%fa:spinner .pulse%{ name }</p>
-			<p class="status"><span class="initing" if={ progress == undefined }>%i18n:common.tags.mk-uploader.waiting%<mk-ellipsis/></span><span class="kb" if={ progress != undefined }>{ String(Math.floor(progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }<i>KB</i> / { String(Math.floor(progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }<i>KB</i></span><span class="percentage" if={ progress != undefined }>{ Math.floor((progress.value / progress.max) * 100) }</span></p>
-			<progress if={ progress != undefined && progress.value != progress.max } value={ progress.value } max={ progress.max }></progress>
-			<div class="progress initing" if={ progress == undefined }></div>
-			<div class="progress waiting" if={ progress != undefined && progress.value == progress.max }></div>
+			<p class="status"><span class="initing" v-if="progress == undefined">%i18n:common.tags.mk-uploader.waiting%<mk-ellipsis/></span><span class="kb" v-if="progress != undefined">{ String(Math.floor(progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }<i>KB</i> / { String(Math.floor(progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }<i>KB</i></span><span class="percentage" v-if="progress != undefined">{ Math.floor((progress.value / progress.max) * 100) }</span></p>
+			<progress v-if="progress != undefined && progress.value != progress.max" value={ progress.value } max={ progress.max }></progress>
+			<div class="progress initing" v-if="progress == undefined"></div>
+			<div class="progress waiting" v-if="progress != undefined && progress.value == progress.max"></div>
 		</li>
 	</ol>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/autocomplete-suggestion.tag b/src/web/app/desktop/tags/autocomplete-suggestion.tag
index 843b3a798..ec531a1b2 100644
--- a/src/web/app/desktop/tags/autocomplete-suggestion.tag
+++ b/src/web/app/desktop/tags/autocomplete-suggestion.tag
@@ -1,5 +1,5 @@
 <mk-autocomplete-suggestion>
-	<ol class="users" ref="users" if={ users.length > 0 }>
+	<ol class="users" ref="users" v-if="users.length > 0">
 		<li each={ users } @click="parent.onClick" onkeydown={ parent.onKeydown } tabindex="-1">
 			<img class="avatar" src={ avatar_url + '?thumbnail&size=32' } alt=""/>
 			<span class="name">{ name }</span>
diff --git a/src/web/app/desktop/tags/big-follow-button.tag b/src/web/app/desktop/tags/big-follow-button.tag
index f2e9dc656..faac04a9f 100644
--- a/src/web/app/desktop/tags/big-follow-button.tag
+++ b/src/web/app/desktop/tags/big-follow-button.tag
@@ -1,10 +1,10 @@
 <mk-big-follow-button>
-	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } if={ !init } @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
-		<span if={ !wait && user.is_following }>%fa:minus%フォロー解除</span>
-		<span if={ !wait && !user.is_following }>%fa:plus%フォロー</span>
-		<virtual if={ wait }>%fa:spinner .pulse .fw%</virtual>
+	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } v-if="!init" @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
+		<span v-if="!wait && user.is_following">%fa:minus%フォロー解除</span>
+		<span v-if="!wait && !user.is_following">%fa:plus%フォロー</span>
+		<virtual v-if="wait">%fa:spinner .pulse .fw%</virtual>
 	</button>
-	<div class="init" if={ init }>%fa:spinner .pulse .fw%</div>
+	<div class="init" v-if="init">%fa:spinner .pulse .fw%</div>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/detailed-post-window.tag b/src/web/app/desktop/tags/detailed-post-window.tag
index 50b4bf920..d5042612c 100644
--- a/src/web/app/desktop/tags/detailed-post-window.tag
+++ b/src/web/app/desktop/tags/detailed-post-window.tag
@@ -1,6 +1,6 @@
 <mk-detailed-post-window>
 	<div class="bg" ref="bg" @click="bgClick"></div>
-	<div class="main" ref="main" if={ !fetching }>
+	<div class="main" ref="main" v-if="!fetching">
 		<mk-post-detail ref="detail" post={ post }/>
 	</div>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/drive/browser-window.tag b/src/web/app/desktop/tags/drive/browser-window.tag
index 4285992f6..af225e00c 100644
--- a/src/web/app/desktop/tags/drive/browser-window.tag
+++ b/src/web/app/desktop/tags/drive/browser-window.tag
@@ -1,7 +1,7 @@
 <mk-drive-browser-window>
 	<mk-window ref="window" is-modal={ false } width={ '800px' } height={ '500px' } popout={ popout }>
 		<yield to="header">
-			<p class="info" if={ parent.usage }><b>{ parent.usage.toFixed(1) }%</b> %i18n:desktop.tags.mk-drive-browser-window.used%</p>
+			<p class="info" v-if="parent.usage"><b>{ parent.usage.toFixed(1) }%</b> %i18n:desktop.tags.mk-drive-browser-window.used%</p>
 			%fa:cloud%%i18n:desktop.tags.mk-drive-browser-window.drive%
 		</yield>
 		<yield to="content">
diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/tags/drive/browser.tag
index 0c0ce4bb4..9b9a42cc2 100644
--- a/src/web/app/desktop/tags/drive/browser.tag
+++ b/src/web/app/desktop/tags/drive/browser.tag
@@ -6,44 +6,44 @@
 				<span class="separator">%fa:angle-right%</span>
 				<mk-drive-browser-nav-folder folder={ folder }/>
 			</virtual>
-			<span class="separator" if={ folder != null }>%fa:angle-right%</span>
-			<span class="folder current" if={ folder != null }>{ folder.name }</span>
+			<span class="separator" v-if="folder != null">%fa:angle-right%</span>
+			<span class="folder current" v-if="folder != null">{ folder.name }</span>
 		</div>
 		<input class="search" type="search" placeholder="&#xf002; %i18n:desktop.tags.mk-drive-browser.search%"/>
 	</nav>
 	<div class="main { uploading: uploads.length > 0, fetching: fetching }" ref="main" onmousedown={ onmousedown } ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop } oncontextmenu={ oncontextmenu }>
 		<div class="selection" ref="selection"></div>
 		<div class="contents" ref="contents">
-			<div class="folders" ref="foldersContainer" if={ folders.length > 0 }>
+			<div class="folders" ref="foldersContainer" v-if="folders.length > 0">
 				<virtual each={ folder in folders }>
 					<mk-drive-browser-folder class="folder" folder={ folder }/>
 				</virtual>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 				<div class="padding" each={ Array(10).fill(16) }></div>
-				<button if={ moreFolders }>%i18n:desktop.tags.mk-drive-browser.load-more%</button>
+				<button v-if="moreFolders">%i18n:desktop.tags.mk-drive-browser.load-more%</button>
 			</div>
-			<div class="files" ref="filesContainer" if={ files.length > 0 }>
+			<div class="files" ref="filesContainer" v-if="files.length > 0">
 				<virtual each={ file in files }>
 					<mk-drive-browser-file class="file" file={ file }/>
 				</virtual>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 				<div class="padding" each={ Array(10).fill(16) }></div>
-				<button if={ moreFiles } @click="fetchMoreFiles">%i18n:desktop.tags.mk-drive-browser.load-more%</button>
+				<button v-if="moreFiles" @click="fetchMoreFiles">%i18n:desktop.tags.mk-drive-browser.load-more%</button>
 			</div>
-			<div class="empty" if={ files.length == 0 && folders.length == 0 && !fetching }>
-				<p if={ draghover }>%i18n:desktop.tags.mk-drive-browser.empty-draghover%</p>
-				<p if={ !draghover && folder == null }><strong>%i18n:desktop.tags.mk-drive-browser.empty-drive%</strong><br/>%i18n:desktop.tags.mk-drive-browser.empty-drive-description%</p>
-				<p if={ !draghover && folder != null }>%i18n:desktop.tags.mk-drive-browser.empty-folder%</p>
+			<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
+				<p v-if="draghover">%i18n:desktop.tags.mk-drive-browser.empty-draghover%</p>
+				<p v-if="!draghover && folder == null"><strong>%i18n:desktop.tags.mk-drive-browser.empty-drive%</strong><br/>%i18n:desktop.tags.mk-drive-browser.empty-drive-description%</p>
+				<p v-if="!draghover && folder != null">%i18n:desktop.tags.mk-drive-browser.empty-folder%</p>
 			</div>
 		</div>
-		<div class="fetching" if={ fetching }>
+		<div class="fetching" v-if="fetching">
 			<div class="spinner">
 				<div class="dot1"></div>
 				<div class="dot2"></div>
 			</div>
 		</div>
 	</div>
-	<div class="dropzone" if={ draghover }></div>
+	<div class="dropzone" v-if="draghover"></div>
 	<mk-uploader ref="uploader"/>
 	<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" onchange={ changeFileInput }/>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/drive/file.tag b/src/web/app/desktop/tags/drive/file.tag
index b33e3bc5e..c55953cc7 100644
--- a/src/web/app/desktop/tags/drive/file.tag
+++ b/src/web/app/desktop/tags/drive/file.tag
@@ -1,14 +1,14 @@
 <mk-drive-browser-file data-is-selected={ isSelected } data-is-contextmenu-showing={ isContextmenuShowing.toString() } @click="onclick" oncontextmenu={ oncontextmenu } draggable="true" ondragstart={ ondragstart } ondragend={ ondragend } title={ title }>
-	<div class="label" if={ I.avatar_id == file.id }><img src="/assets/label.svg"/>
+	<div class="label" v-if="I.avatar_id == file.id"><img src="/assets/label.svg"/>
 		<p>%i18n:desktop.tags.mk-drive-browser-file.avatar%</p>
 	</div>
-	<div class="label" if={ I.banner_id == file.id }><img src="/assets/label.svg"/>
+	<div class="label" v-if="I.banner_id == file.id"><img src="/assets/label.svg"/>
 		<p>%i18n:desktop.tags.mk-drive-browser-file.banner%</p>
 	</div>
 	<div class="thumbnail" ref="thumbnail" style="background-color:{ file.properties.average_color ? 'rgb(' + file.properties.average_color.join(',') + ')' : 'transparent' }">
 		<img src={ file.url + '?thumbnail&size=128' } alt="" onload={ onload }/>
 	</div>
-	<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
+	<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" v-if="file.name.lastIndexOf('.') != -1">{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/drive/folder.tag b/src/web/app/desktop/tags/drive/folder.tag
index 9458671cd..90d9f2b3c 100644
--- a/src/web/app/desktop/tags/drive/folder.tag
+++ b/src/web/app/desktop/tags/drive/folder.tag
@@ -1,5 +1,5 @@
 <mk-drive-browser-folder data-is-contextmenu-showing={ isContextmenuShowing.toString() } data-draghover={ draghover.toString() } @click="onclick" onmouseover={ onmouseover } onmouseout={ onmouseout } ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop } oncontextmenu={ oncontextmenu } draggable="true" ondragstart={ ondragstart } ondragend={ ondragend } title={ title }>
-	<p class="name"><virtual if={ hover }>%fa:R folder-open .fw%</virtual><virtual if={ !hover }>%fa:R folder .fw%</virtual>{ folder.name }</p>
+	<p class="name"><virtual v-if="hover">%fa:R folder-open .fw%</virtual><virtual v-if="!hover">%fa:R folder .fw%</virtual>{ folder.name }</p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/drive/nav-folder.tag b/src/web/app/desktop/tags/drive/nav-folder.tag
index f16cf2181..9c943f26e 100644
--- a/src/web/app/desktop/tags/drive/nav-folder.tag
+++ b/src/web/app/desktop/tags/drive/nav-folder.tag
@@ -1,5 +1,5 @@
 <mk-drive-browser-nav-folder data-draghover={ draghover } @click="onclick" ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop }>
-	<virtual if={ folder == null }>%fa:cloud%</virtual><span>{ folder == null ? '%i18n:desktop.tags.mk-drive-browser-nav-folder.drive%' : folder.name }</span>
+	<virtual v-if="folder == null">%fa:cloud%</virtual><span>{ folder == null ? '%i18n:desktop.tags.mk-drive-browser-nav-folder.drive%' : folder.name }</span>
 	<style lang="stylus" scoped>
 		:scope
 			&[data-draghover]
diff --git a/src/web/app/desktop/tags/follow-button.tag b/src/web/app/desktop/tags/follow-button.tag
index 5e482509a..aa7e34321 100644
--- a/src/web/app/desktop/tags/follow-button.tag
+++ b/src/web/app/desktop/tags/follow-button.tag
@@ -1,10 +1,10 @@
 <mk-follow-button>
-	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } if={ !init } @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
-		<virtual if={ !wait && user.is_following }>%fa:minus%</virtual>
-		<virtual if={ !wait && !user.is_following }>%fa:plus%</virtual>
-		<virtual if={ wait }>%fa:spinner .pulse .fw%</virtual>
+	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } v-if="!init" @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
+		<virtual v-if="!wait && user.is_following">%fa:minus%</virtual>
+		<virtual v-if="!wait && !user.is_following">%fa:plus%</virtual>
+		<virtual v-if="wait">%fa:spinner .pulse .fw%</virtual>
 	</button>
-	<div class="init" if={ init }>%fa:spinner .pulse .fw%</div>
+	<div class="init" v-if="init">%fa:spinner .pulse .fw%</div>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/following-setuper.tag b/src/web/app/desktop/tags/following-setuper.tag
index 9453b5bf5..8aeb8a3f0 100644
--- a/src/web/app/desktop/tags/following-setuper.tag
+++ b/src/web/app/desktop/tags/following-setuper.tag
@@ -1,6 +1,6 @@
 <mk-following-setuper>
 	<p class="title">気になるユーザーをフォロー:</p>
-	<div class="users" if={ !fetching && users.length > 0 }>
+	<div class="users" v-if="!fetching && users.length > 0">
 		<div class="user" each={ users }><a class="avatar-anchor" href={ '/' + username }><img class="avatar" src={ avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ id }/></a>
 			<div class="body"><a class="name" href={ '/' + username } target="_blank" data-user-preview={ id }>{ name }</a>
 				<p class="username">@{ username }</p>
@@ -8,8 +8,8 @@
 			<mk-follow-button user={ this }/>
 		</div>
 	</div>
-	<p class="empty" if={ !fetching && users.length == 0 }>おすすめのユーザーは見つかりませんでした。</p>
-	<p class="fetching" if={ fetching }>%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
+	<p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
 	<a class="refresh" @click="refresh">もっと見る</a>
 	<button class="close" @click="close" title="閉じる">%fa:times%</button>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/home-widgets/access-log.tag b/src/web/app/desktop/tags/home-widgets/access-log.tag
index 47a6fd350..1e9ea0fdb 100644
--- a/src/web/app/desktop/tags/home-widgets/access-log.tag
+++ b/src/web/app/desktop/tags/home-widgets/access-log.tag
@@ -1,5 +1,5 @@
 <mk-access-log-home-widget>
-	<virtual if={ data.design == 0 }>
+	<virtual v-if="data.design == 0">
 		<p class="title">%fa:server%%i18n:desktop.tags.mk-access-log-home-widget.title%</p>
 	</virtual>
 	<div ref="log">
diff --git a/src/web/app/desktop/tags/home-widgets/broadcast.tag b/src/web/app/desktop/tags/home-widgets/broadcast.tag
index a1bd2175d..963b31237 100644
--- a/src/web/app/desktop/tags/home-widgets/broadcast.tag
+++ b/src/web/app/desktop/tags/home-widgets/broadcast.tag
@@ -8,12 +8,12 @@
 			<path class="wave d" d="M29.18,1.06c-0.479-0.502-1.273-0.522-1.775-0.044c-0.016,0.015-0.029,0.029-0.045,0.044c-0.5,0.52-0.5,1.36,0,1.88 c1.361,1.4,2.041,3.24,2.041,5.08s-0.68,3.66-2.041,5.08c-0.5,0.52-0.5,1.36,0,1.88c0.509,0.508,1.332,0.508,1.841,0 c1.86-1.92,2.8-4.44,2.8-6.96C31.99,5.424,30.98,2.931,29.18,1.06z"></path>
 		</svg>
 	</div>
-	<p class="fetching" if={ fetching }>%i18n:desktop.tags.mk-broadcast-home-widget.fetching%<mk-ellipsis/></p>
-	<h1 if={ !fetching }>{
+	<p class="fetching" v-if="fetching">%i18n:desktop.tags.mk-broadcast-home-widget.fetching%<mk-ellipsis/></p>
+	<h1 v-if="!fetching">{
 		broadcasts.length == 0 ? '%i18n:desktop.tags.mk-broadcast-home-widget.no-broadcasts%' : broadcasts[i].title
 	}</h1>
-	<p if={ !fetching }><mk-raw if={ broadcasts.length != 0 } content={ broadcasts[i].text }/><virtual if={ broadcasts.length == 0 }>%i18n:desktop.tags.mk-broadcast-home-widget.have-a-nice-day%</virtual></p>
-	<a if={ broadcasts.length > 1 } @click="next">%i18n:desktop.tags.mk-broadcast-home-widget.next% &gt;&gt;</a>
+	<p v-if="!fetching"><mk-raw v-if="broadcasts.length != 0" content={ broadcasts[i].text }/><virtual v-if="broadcasts.length == 0">%i18n:desktop.tags.mk-broadcast-home-widget.have-a-nice-day%</virtual></p>
+	<a v-if="broadcasts.length > 1" @click="next">%i18n:desktop.tags.mk-broadcast-home-widget.next% &gt;&gt;</a>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/home-widgets/channel.tag b/src/web/app/desktop/tags/home-widgets/channel.tag
index 60227a629..3fc1f1abf 100644
--- a/src/web/app/desktop/tags/home-widgets/channel.tag
+++ b/src/web/app/desktop/tags/home-widgets/channel.tag
@@ -1,11 +1,11 @@
 <mk-channel-home-widget>
-	<virtual if={ !data.compact }>
+	<virtual v-if="!data.compact">
 		<p class="title">%fa:tv%{
 			channel ? channel.title : '%i18n:desktop.tags.mk-channel-home-widget.title%'
 		}</p>
 		<button @click="settings" title="%i18n:desktop.tags.mk-channel-home-widget.settings%">%fa:cog%</button>
 	</virtual>
-	<p class="get-started" if={ this.data.channel == null }>%i18n:desktop.tags.mk-channel-home-widget.get-started%</p>
+	<p class="get-started" v-if="this.data.channel == null">%i18n:desktop.tags.mk-channel-home-widget.get-started%</p>
 	<mk-channel ref="channel" show={ this.data.channel }/>
 	<style lang="stylus" scoped>
 		:scope
@@ -104,9 +104,9 @@
 </mk-channel-home-widget>
 
 <mk-channel>
-	<p if={ fetching }>読み込み中<mk-ellipsis/></p>
-	<div if={ !fetching } ref="posts">
-		<p if={ posts.length == 0 }>まだ投稿がありません</p>
+	<p v-if="fetching">読み込み中<mk-ellipsis/></p>
+	<div v-if="!fetching" ref="posts">
+		<p v-if="posts.length == 0">まだ投稿がありません</p>
 		<mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/>
 	</div>
 	<mk-channel-form ref="form"/>
@@ -197,9 +197,9 @@
 		<span>ID:<i>{ post.user.username }</i></span>
 	</header>
 	<div>
-		<a if={ post.reply }>&gt;&gt;{ post.reply.index }</a>
+		<a v-if="post.reply">&gt;&gt;{ post.reply.index }</a>
 		{ post.text }
-		<div class="media" if={ post.media }>
+		<div class="media" v-if="post.media">
 			<virtual each={ file in post.media }>
 				<a href={ file.url } target="_blank">
 					<img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/tags/home-widgets/mentions.tag
index 519e124ae..d4569216c 100644
--- a/src/web/app/desktop/tags/home-widgets/mentions.tag
+++ b/src/web/app/desktop/tags/home-widgets/mentions.tag
@@ -1,13 +1,13 @@
 <mk-mentions-home-widget>
 	<header><span data-is-active={ mode == 'all' } @click="setMode.bind(this, 'all')">すべて</span><span data-is-active={ mode == 'following' } @click="setMode.bind(this, 'following')">フォロー中</span></header>
-	<div class="loading" if={ isLoading }>
+	<div class="loading" v-if="isLoading">
 		<mk-ellipsis-icon/>
 	</div>
-	<p class="empty" if={ isEmpty }>%fa:R comments%<span if={ mode == 'all' }>あなた宛ての投稿はありません。</span><span if={ mode == 'following' }>あなたがフォローしているユーザーからの言及はありません。</span></p>
+	<p class="empty" v-if="isEmpty">%fa:R comments%<span v-if="mode == 'all'">あなた宛ての投稿はありません。</span><span v-if="mode == 'following'">あなたがフォローしているユーザーからの言及はありません。</span></p>
 	<mk-timeline ref="timeline">
 		<yield to="footer">
-			<virtual if={ !parent.moreLoading }>%fa:moon%</virtual>
-			<virtual if={ parent.moreLoading }>%fa:spinner .pulse .fw%</virtual>
+			<virtual v-if="!parent.moreLoading">%fa:moon%</virtual>
+			<virtual v-if="parent.moreLoading">%fa:spinner .pulse .fw%</virtual>
 		</yield/>
 	</mk-timeline>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/home-widgets/messaging.tag b/src/web/app/desktop/tags/home-widgets/messaging.tag
index 53f4e2f06..b5edd36fd 100644
--- a/src/web/app/desktop/tags/home-widgets/messaging.tag
+++ b/src/web/app/desktop/tags/home-widgets/messaging.tag
@@ -1,5 +1,5 @@
 <mk-messaging-home-widget>
-	<virtual if={ data.design == 0 }>
+	<virtual v-if="data.design == 0">
 		<p class="title">%fa:comments%%i18n:desktop.tags.mk-messaging-home-widget.title%</p>
 	</virtual>
 	<mk-messaging ref="index" compact={ true }/>
diff --git a/src/web/app/desktop/tags/home-widgets/notifications.tag b/src/web/app/desktop/tags/home-widgets/notifications.tag
index 31ef6f608..4a6d7b417 100644
--- a/src/web/app/desktop/tags/home-widgets/notifications.tag
+++ b/src/web/app/desktop/tags/home-widgets/notifications.tag
@@ -1,5 +1,5 @@
 <mk-notifications-home-widget>
-	<virtual if={ !data.compact }>
+	<virtual v-if="!data.compact">
 		<p class="title">%fa:R bell%%i18n:desktop.tags.mk-notifications-home-widget.title%</p>
 		<button @click="settings" title="%i18n:desktop.tags.mk-notifications-home-widget.settings%">%fa:cog%</button>
 	</virtual>
diff --git a/src/web/app/desktop/tags/home-widgets/photo-stream.tag b/src/web/app/desktop/tags/home-widgets/photo-stream.tag
index 80f0573fb..6040e4611 100644
--- a/src/web/app/desktop/tags/home-widgets/photo-stream.tag
+++ b/src/web/app/desktop/tags/home-widgets/photo-stream.tag
@@ -1,14 +1,14 @@
 <mk-photo-stream-home-widget data-melt={ data.design == 2 }>
-	<virtual if={ data.design == 0 }>
+	<virtual v-if="data.design == 0">
 		<p class="title">%fa:camera%%i18n:desktop.tags.mk-photo-stream-home-widget.title%</p>
 	</virtual>
-	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<div class="stream" if={ !initializing && images.length > 0 }>
+	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<div class="stream" v-if="!initializing && images.length > 0">
 		<virtual each={ image in images }>
 			<div class="img" style={ 'background-image: url(' + image.url + '?thumbnail&size=256)' }></div>
 		</virtual>
 	</div>
-	<p class="empty" if={ !initializing && images.length == 0 }>%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p>
+	<p class="empty" v-if="!initializing && images.length == 0">%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/home-widgets/post-form.tag b/src/web/app/desktop/tags/home-widgets/post-form.tag
index b20a1c361..a3dc3dd6e 100644
--- a/src/web/app/desktop/tags/home-widgets/post-form.tag
+++ b/src/web/app/desktop/tags/home-widgets/post-form.tag
@@ -1,7 +1,7 @@
 <mk-post-form-home-widget>
-	<mk-post-form if={ place == 'main' }/>
-	<virtual if={ place != 'main' }>
-		<virtual if={ data.design == 0 }>
+	<mk-post-form v-if="place == 'main'"/>
+	<virtual v-if="place != 'main'">
+		<virtual v-if="data.design == 0">
 			<p class="title">%fa:pencil-alt%%i18n:desktop.tags.mk-post-form-home-widget.title%</p>
 		</virtual>
 		<textarea disabled={ posting } ref="text" onkeydown={ onkeydown } placeholder="%i18n:desktop.tags.mk-post-form-home-widget.placeholder%"></textarea>
diff --git a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag b/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
index 3abb35fac..cf76ea9c1 100644
--- a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
+++ b/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
@@ -1,15 +1,15 @@
 <mk-recommended-polls-home-widget>
-	<virtual if={ !data.compact }>
+	<virtual v-if="!data.compact">
 		<p class="title">%fa:chart-pie%%i18n:desktop.tags.mk-recommended-polls-home-widget.title%</p>
 		<button @click="fetch" title="%i18n:desktop.tags.mk-recommended-polls-home-widget.refresh%">%fa:sync%</button>
 	</virtual>
-	<div class="poll" if={ !loading && poll != null }>
-		<p if={ poll.text }><a href="/{ poll.user.username }/{ poll.id }">{ poll.text }</a></p>
-		<p if={ !poll.text }><a href="/{ poll.user.username }/{ poll.id }">%fa:link%</a></p>
+	<div class="poll" v-if="!loading && poll != null">
+		<p v-if="poll.text"><a href="/{ poll.user.username }/{ poll.id }">{ poll.text }</a></p>
+		<p v-if="!poll.text"><a href="/{ poll.user.username }/{ poll.id }">%fa:link%</a></p>
 		<mk-poll post={ poll }/>
 	</div>
-	<p class="empty" if={ !loading && poll == null }>%i18n:desktop.tags.mk-recommended-polls-home-widget.nothing%</p>
-	<p class="loading" if={ loading }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<p class="empty" v-if="!loading && poll == null">%i18n:desktop.tags.mk-recommended-polls-home-widget.nothing%</p>
+	<p class="loading" v-if="loading">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/home-widgets/rss-reader.tag b/src/web/app/desktop/tags/home-widgets/rss-reader.tag
index 524d0c110..916281def 100644
--- a/src/web/app/desktop/tags/home-widgets/rss-reader.tag
+++ b/src/web/app/desktop/tags/home-widgets/rss-reader.tag
@@ -1,12 +1,12 @@
 <mk-rss-reader-home-widget>
-	<virtual if={ !data.compact }>
+	<virtual v-if="!data.compact">
 		<p class="title">%fa:rss-square%RSS</p>
 		<button @click="settings" title="設定">%fa:cog%</button>
 	</virtual>
-	<div class="feed" if={ !initializing }>
+	<div class="feed" v-if="!initializing">
 		<virtual each={ item in items }><a href={ item.link } target="_blank">{ item.title }</a></virtual>
 	</div>
-	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/home-widgets/server.tag b/src/web/app/desktop/tags/home-widgets/server.tag
index f716c9dfe..cae2306a5 100644
--- a/src/web/app/desktop/tags/home-widgets/server.tag
+++ b/src/web/app/desktop/tags/home-widgets/server.tag
@@ -1,15 +1,15 @@
 <mk-server-home-widget data-melt={ data.design == 2 }>
-	<virtual if={ data.design == 0 }>
+	<virtual v-if="data.design == 0">
 		<p class="title">%fa:server%%i18n:desktop.tags.mk-server-home-widget.title%</p>
 		<button @click="toggle" title="%i18n:desktop.tags.mk-server-home-widget.toggle%">%fa:sort%</button>
 	</virtual>
-	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<mk-server-home-widget-cpu-and-memory-usage if={ !initializing } show={ data.view == 0 } connection={ connection }/>
-	<mk-server-home-widget-cpu if={ !initializing } show={ data.view == 1 } connection={ connection } meta={ meta }/>
-	<mk-server-home-widget-memory if={ !initializing } show={ data.view == 2 } connection={ connection }/>
-	<mk-server-home-widget-disk if={ !initializing } show={ data.view == 3 } connection={ connection }/>
-	<mk-server-home-widget-uptimes if={ !initializing } show={ data.view == 4 } connection={ connection }/>
-	<mk-server-home-widget-info if={ !initializing } show={ data.view == 5 } connection={ connection } meta={ meta }/>
+	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<mk-server-home-widget-cpu-and-memory-usage v-if="!initializing" show={ data.view == 0 } connection={ connection }/>
+	<mk-server-home-widget-cpu v-if="!initializing" show={ data.view == 1 } connection={ connection } meta={ meta }/>
+	<mk-server-home-widget-memory v-if="!initializing" show={ data.view == 2 } connection={ connection }/>
+	<mk-server-home-widget-disk v-if="!initializing" show={ data.view == 3 } connection={ connection }/>
+	<mk-server-home-widget-uptimes v-if="!initializing" show={ data.view == 4 } connection={ connection }/>
+	<mk-server-home-widget-info v-if="!initializing" show={ data.view == 5 } connection={ connection } meta={ meta }/>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/home-widgets/slideshow.tag b/src/web/app/desktop/tags/home-widgets/slideshow.tag
index c356f5cbd..ab78ca2c6 100644
--- a/src/web/app/desktop/tags/home-widgets/slideshow.tag
+++ b/src/web/app/desktop/tags/home-widgets/slideshow.tag
@@ -1,7 +1,7 @@
 <mk-slideshow-home-widget>
 	<div @click="choose">
-		<p if={ data.folder === undefined }>クリックしてフォルダを指定してください</p>
-		<p if={ data.folder !== undefined && images.length == 0 && !fetching }>このフォルダには画像がありません</p>
+		<p v-if="data.folder === undefined">クリックしてフォルダを指定してください</p>
+		<p v-if="data.folder !== undefined && images.length == 0 && !fetching">このフォルダには画像がありません</p>
 		<div ref="slideA" class="slide a"></div>
 		<div ref="slideB" class="slide b"></div>
 	</div>
diff --git a/src/web/app/desktop/tags/home-widgets/timeline.tag b/src/web/app/desktop/tags/home-widgets/timeline.tag
index 4d3d830ce..2bbee14fa 100644
--- a/src/web/app/desktop/tags/home-widgets/timeline.tag
+++ b/src/web/app/desktop/tags/home-widgets/timeline.tag
@@ -1,13 +1,13 @@
 <mk-timeline-home-widget>
-	<mk-following-setuper if={ noFollowing }/>
-	<div class="loading" if={ isLoading }>
+	<mk-following-setuper v-if="noFollowing"/>
+	<div class="loading" v-if="isLoading">
 		<mk-ellipsis-icon/>
 	</div>
-	<p class="empty" if={ isEmpty && !isLoading }>%fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。</p>
+	<p class="empty" v-if="isEmpty && !isLoading">%fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。</p>
 	<mk-timeline ref="timeline" hide={ isLoading }>
 		<yield to="footer">
-			<virtual if={ !parent.moreLoading }>%fa:moon%</virtual>
-			<virtual if={ parent.moreLoading }>%fa:spinner .pulse .fw%</virtual>
+			<virtual v-if="!parent.moreLoading">%fa:moon%</virtual>
+			<virtual v-if="parent.moreLoading">%fa:spinner .pulse .fw%</virtual>
 		</yield/>
 	</mk-timeline>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/home-widgets/trends.tag b/src/web/app/desktop/tags/home-widgets/trends.tag
index 0d8454da6..db2ed9510 100644
--- a/src/web/app/desktop/tags/home-widgets/trends.tag
+++ b/src/web/app/desktop/tags/home-widgets/trends.tag
@@ -1,14 +1,14 @@
 <mk-trends-home-widget>
-	<virtual if={ !data.compact }>
+	<virtual v-if="!data.compact">
 		<p class="title">%fa:fire%%i18n:desktop.tags.mk-trends-home-widget.title%</p>
 		<button @click="fetch" title="%i18n:desktop.tags.mk-trends-home-widget.refresh%">%fa:sync%</button>
 	</virtual>
-	<div class="post" if={ !loading && post != null }>
+	<div class="post" v-if="!loading && post != null">
 		<p class="text"><a href="/{ post.user.username }/{ post.id }">{ post.text }</a></p>
 		<p class="author">―<a href="/{ post.user.username }">@{ post.user.username }</a></p>
 	</div>
-	<p class="empty" if={ !loading && post == null }>%i18n:desktop.tags.mk-trends-home-widget.nothing%</p>
-	<p class="loading" if={ loading }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<p class="empty" v-if="!loading && post == null">%i18n:desktop.tags.mk-trends-home-widget.nothing%</p>
+	<p class="loading" v-if="loading">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
index 763d39449..25a60b95a 100644
--- a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
+++ b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
@@ -1,9 +1,9 @@
 <mk-user-recommendation-home-widget>
-	<virtual if={ !data.compact }>
+	<virtual v-if="!data.compact">
 		<p class="title">%fa:users%%i18n:desktop.tags.mk-user-recommendation-home-widget.title%</p>
 		<button @click="refresh" title="%i18n:desktop.tags.mk-user-recommendation-home-widget.refresh%">%fa:sync%</button>
 	</virtual>
-	<div class="user" if={ !loading && users.length != 0 } each={ _user in users }>
+	<div class="user" v-if="!loading && users.length != 0" each={ _user in users }>
 		<a class="avatar-anchor" href={ '/' + _user.username }>
 			<img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ _user.id }/>
 		</a>
@@ -13,8 +13,8 @@
 		</div>
 		<mk-follow-button user={ _user }/>
 	</div>
-	<p class="empty" if={ !loading && users.length == 0 }>%i18n:desktop.tags.mk-user-recommendation-home-widget.no-one%</p>
-	<p class="loading" if={ loading }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<p class="empty" v-if="!loading && users.length == 0">%i18n:desktop.tags.mk-user-recommendation-home-widget.no-one%</p>
+	<p class="loading" v-if="loading">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/home.tag b/src/web/app/desktop/tags/home.tag
index e54acd18e..f727c3e80 100644
--- a/src/web/app/desktop/tags/home.tag
+++ b/src/web/app/desktop/tags/home.tag
@@ -1,5 +1,5 @@
 <mk-home data-customize={ opts.customize }>
-	<div class="customize" if={ opts.customize }>
+	<div class="customize" v-if="opts.customize">
 		<a href="/">%fa:check%完了</a>
 		<div>
 			<div class="adder">
@@ -40,9 +40,9 @@
 			<div ref="left" data-place="left"></div>
 		</div>
 		<main ref="main">
-			<div class="maintop" ref="maintop" data-place="main" if={ opts.customize }></div>
-			<mk-timeline-home-widget ref="tl" if={ mode == 'timeline' }/>
-			<mk-mentions-home-widget ref="tl" if={ mode == 'mentions' }/>
+			<div class="maintop" ref="maintop" data-place="main" v-if="opts.customize"></div>
+			<mk-timeline-home-widget ref="tl" v-if="mode == 'timeline'"/>
+			<mk-mentions-home-widget ref="tl" v-if="mode == 'mentions'"/>
 		</main>
 		<div class="right">
 			<div ref="right" data-place="right"></div>
diff --git a/src/web/app/desktop/tags/list-user.tag b/src/web/app/desktop/tags/list-user.tag
index c0e1051d1..45c4deb53 100644
--- a/src/web/app/desktop/tags/list-user.tag
+++ b/src/web/app/desktop/tags/list-user.tag
@@ -8,7 +8,7 @@
 			<span class="username">@{ user.username }</span>
 		</header>
 		<div class="body">
-			<p class="followed" if={ user.is_followed }>フォローされています</p>
+			<p class="followed" v-if="user.is_followed">フォローされています</p>
 			<div class="description">{ user.description }</div>
 		</div>
 	</div>
diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag
index 99024473f..6a16db135 100644
--- a/src/web/app/desktop/tags/notifications.tag
+++ b/src/web/app/desktop/tags/notifications.tag
@@ -1,9 +1,9 @@
 <mk-notifications>
-	<div class="notifications" if={ notifications.length != 0 }>
+	<div class="notifications" v-if="notifications.length != 0">
 		<virtual each={ notification, i in notifications }>
 			<div class="notification { notification.type }">
 				<mk-time time={ notification.created_at }/>
-				<virtual if={ notification.type == 'reaction' }>
+				<virtual v-if="notification.type == 'reaction'">
 					<a class="avatar-anchor" href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>
 						<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
 					</a>
@@ -14,7 +14,7 @@
 						</a>
 					</div>
 				</virtual>
-				<virtual if={ notification.type == 'repost' }>
+				<virtual v-if="notification.type == 'repost'">
 					<a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>
 						<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
 					</a>
@@ -25,7 +25,7 @@
 						</a>
 					</div>
 				</virtual>
-				<virtual if={ notification.type == 'quote' }>
+				<virtual v-if="notification.type == 'quote'">
 					<a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>
 						<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
 					</a>
@@ -34,7 +34,7 @@
 						<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
 					</div>
 				</virtual>
-				<virtual if={ notification.type == 'follow' }>
+				<virtual v-if="notification.type == 'follow'">
 					<a class="avatar-anchor" href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>
 						<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
 					</a>
@@ -42,7 +42,7 @@
 						<p>%fa:user-plus%<a href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>{ notification.user.name }</a></p>
 					</div>
 				</virtual>
-				<virtual if={ notification.type == 'reply' }>
+				<virtual v-if="notification.type == 'reply'">
 					<a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>
 						<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
 					</a>
@@ -51,7 +51,7 @@
 						<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
 					</div>
 				</virtual>
-				<virtual if={ notification.type == 'mention' }>
+				<virtual v-if="notification.type == 'mention'">
 					<a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>
 						<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
 					</a>
@@ -60,7 +60,7 @@
 						<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
 					</div>
 				</virtual>
-				<virtual if={ notification.type == 'poll_vote' }>
+				<virtual v-if="notification.type == 'poll_vote'">
 					<a class="avatar-anchor" href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>
 						<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
 					</a>
@@ -72,17 +72,17 @@
 					</div>
 				</virtual>
 			</div>
-			<p class="date" if={ i != notifications.length - 1 && notification._date != notifications[i + 1]._date }>
+			<p class="date" v-if="i != notifications.length - 1 && notification._date != notifications[i + 1]._date">
 				<span>%fa:angle-up%{ notification._datetext }</span>
 				<span>%fa:angle-down%{ notifications[i + 1]._datetext }</span>
 			</p>
 		</virtual>
 	</div>
-	<button class="more { fetching: fetchingMoreNotifications }" if={ moreNotifications } @click="fetchMoreNotifications" disabled={ fetchingMoreNotifications }>
-		<virtual if={ fetchingMoreNotifications }>%fa:spinner .pulse .fw%</virtual>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:desktop.tags.mk-notifications.more%' }
+	<button class="more { fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" disabled={ fetchingMoreNotifications }>
+		<virtual v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</virtual>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:desktop.tags.mk-notifications.more%' }
 	</button>
-	<p class="empty" if={ notifications.length == 0 && !loading }>ありません!</p>
-	<p class="loading" if={ loading }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<p class="empty" v-if="notifications.length == 0 && !loading">ありません!</p>
+	<p class="loading" v-if="loading">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/tags/pages/entrance.tag
index 9b8b4eca6..c516bdb38 100644
--- a/src/web/app/desktop/tags/pages/entrance.tag
+++ b/src/web/app/desktop/tags/pages/entrance.tag
@@ -3,12 +3,12 @@
 		<div>
 			<h1>どこにいても、ここにあります</h1>
 			<p>ようこそ! MisskeyはTwitter風ミニブログSNSです――思ったこと、共有したいことをシンプルに書き残せます。タイムラインを見れば、皆の反応や皆がどう思っているのかもすぐにわかります。</p>
-			<p if={ stats }>これまでに{ stats.posts_count }投稿されました</p>
+			<p v-if="stats">これまでに{ stats.posts_count }投稿されました</p>
 		</div>
 		<div>
-			<mk-entrance-signin if={ mode == 'signin' }/>
-			<mk-entrance-signup if={ mode == 'signup' }/>
-			<div class="introduction" if={ mode == 'introduction' }>
+			<mk-entrance-signin v-if="mode == 'signin'"/>
+			<mk-entrance-signup v-if="mode == 'signup'"/>
+			<div class="introduction" v-if="mode == 'introduction'">
 				<mk-introduction/>
 				<button @click="signin">わかった</button>
 			</div>
@@ -152,7 +152,7 @@
 <mk-entrance-signin>
 	<a class="help" href={ _DOCS_URL_ + '/help' } title="お困りですか?">%fa:question%</a>
 	<div class="form">
-		<h1><img if={ user } src={ user.avatar_url + '?thumbnail&size=32' }/>
+		<h1><img v-if="user" src={ user.avatar_url + '?thumbnail&size=32' }/>
 			<p>{ user ? user.name : 'アカウント' }</p>
 		</h1>
 		<mk-signin ref="signin"/>
diff --git a/src/web/app/desktop/tags/pages/messaging-room.tag b/src/web/app/desktop/tags/pages/messaging-room.tag
index 48096ec80..54bd38e57 100644
--- a/src/web/app/desktop/tags/pages/messaging-room.tag
+++ b/src/web/app/desktop/tags/pages/messaging-room.tag
@@ -1,5 +1,5 @@
 <mk-messaging-room-page>
-	<mk-messaging-room if={ user } user={ user } is-naked={ true }/>
+	<mk-messaging-room v-if="user" user={ user } is-naked={ true }/>
 
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/desktop/tags/pages/post.tag b/src/web/app/desktop/tags/pages/post.tag
index 43f040ed2..b5cfea3ad 100644
--- a/src/web/app/desktop/tags/pages/post.tag
+++ b/src/web/app/desktop/tags/pages/post.tag
@@ -1,9 +1,9 @@
 <mk-post-page>
 	<mk-ui ref="ui">
-		<main if={ !parent.fetching }>
-			<a if={ parent.post.next } href={ parent.post.next }>%fa:angle-up%%i18n:desktop.tags.mk-post-page.next%</a>
+		<main v-if="!parent.fetching">
+			<a v-if="parent.post.next" href={ parent.post.next }>%fa:angle-up%%i18n:desktop.tags.mk-post-page.next%</a>
 			<mk-post-detail ref="detail" post={ parent.post }/>
-			<a if={ parent.post.prev } href={ parent.post.prev }>%fa:angle-down%%i18n:desktop.tags.mk-post-page.prev%</a>
+			<a v-if="parent.post.prev" href={ parent.post.prev }>%fa:angle-down%%i18n:desktop.tags.mk-post-page.prev%</a>
 		</main>
 	</mk-ui>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/tags/post-detail-sub.tag
index 62f09d4e2..0b8d4d1d3 100644
--- a/src/web/app/desktop/tags/post-detail-sub.tag
+++ b/src/web/app/desktop/tags/post-detail-sub.tag
@@ -16,7 +16,7 @@
 		</header>
 		<div class="body">
 			<div class="text" ref="text"></div>
-			<div class="media" if={ post.media }>
+			<div class="media" v-if="post.media">
 				<mk-images images={ post.media }/>
 			</div>
 		</div>
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index 4ba8275b2..a4f88da7d 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -1,18 +1,18 @@
 <mk-post-detail title={ title }>
 	<div class="main">
-		<button class="read-more" if={ p.reply && p.reply.reply_id && context == null } title="会話をもっと読み込む" @click="loadContext" disabled={ contextFetching }>
-			<virtual if={ !contextFetching }>%fa:ellipsis-v%</virtual>
-			<virtual if={ contextFetching }>%fa:spinner .pulse%</virtual>
+		<button class="read-more" v-if="p.reply && p.reply.reply_id && context == null" title="会話をもっと読み込む" @click="loadContext" disabled={ contextFetching }>
+			<virtual v-if="!contextFetching">%fa:ellipsis-v%</virtual>
+			<virtual v-if="contextFetching">%fa:spinner .pulse%</virtual>
 		</button>
 		<div class="context">
 			<virtual each={ post in context }>
 				<mk-post-detail-sub post={ post }/>
 			</virtual>
 		</div>
-		<div class="reply-to" if={ p.reply }>
+		<div class="reply-to" v-if="p.reply">
 			<mk-post-detail-sub post={ p.reply }/>
 		</div>
-		<div class="repost" if={ isRepost }>
+		<div class="repost" v-if="isRepost">
 			<p>
 				<a class="avatar-anchor" href={ '/' + post.user.username } data-user-preview={ post.user_id }>
 					<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=32' } alt="avatar"/>
@@ -36,28 +36,28 @@
 			</header>
 			<div class="body">
 				<div class="text" ref="text"></div>
-				<div class="media" if={ p.media }>
+				<div class="media" v-if="p.media">
 					<mk-images images={ p.media }/>
 				</div>
-				<mk-poll if={ p.poll } post={ p }/>
+				<mk-poll v-if="p.poll" post={ p }/>
 			</div>
 			<footer>
 				<mk-reactions-viewer post={ p }/>
 				<button @click="reply" title="返信">
-					%fa:reply%<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
+					%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
 				</button>
 				<button @click="repost" title="Repost">
-					%fa:retweet%<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
+					%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
 				</button>
 				<button class={ reacted: p.my_reaction != null } @click="react" ref="reactButton" title="リアクション">
-					%fa:plus%<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
+					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
 				</button>
 				<button @click="menu" ref="menuButton">
 					%fa:ellipsis-h%
 				</button>
 			</footer>
 		</article>
-		<div class="replies" if={ !compact }>
+		<div class="replies" v-if="!compact">
 			<virtual each={ post in replies }>
 				<mk-post-detail-sub post={ post }/>
 			</virtual>
diff --git a/src/web/app/desktop/tags/post-form-window.tag b/src/web/app/desktop/tags/post-form-window.tag
index 184ff548a..80b51df60 100644
--- a/src/web/app/desktop/tags/post-form-window.tag
+++ b/src/web/app/desktop/tags/post-form-window.tag
@@ -1,13 +1,13 @@
 <mk-post-form-window>
 	<mk-window ref="window" is-modal={ true }>
 		<yield to="header">
-			<span if={ !parent.opts.reply }>%i18n:desktop.tags.mk-post-form-window.post%</span>
-			<span if={ parent.opts.reply }>%i18n:desktop.tags.mk-post-form-window.reply%</span>
-			<span class="files" if={ parent.files.length != 0 }>{ '%i18n:desktop.tags.mk-post-form-window.attaches%'.replace('{}', parent.files.length) }</span>
-			<span class="uploading-files" if={ parent.uploadingFiles.length != 0 }>{ '%i18n:desktop.tags.mk-post-form-window.uploading-media%'.replace('{}', parent.uploadingFiles.length) }<mk-ellipsis/></span>
+			<span v-if="!parent.opts.reply">%i18n:desktop.tags.mk-post-form-window.post%</span>
+			<span v-if="parent.opts.reply">%i18n:desktop.tags.mk-post-form-window.reply%</span>
+			<span class="files" v-if="parent.files.length != 0">{ '%i18n:desktop.tags.mk-post-form-window.attaches%'.replace('{}', parent.files.length) }</span>
+			<span class="uploading-files" v-if="parent.uploadingFiles.length != 0">{ '%i18n:desktop.tags.mk-post-form-window.uploading-media%'.replace('{}', parent.uploadingFiles.length) }<mk-ellipsis/></span>
 		</yield>
 		<yield to="content">
-			<div class="ref" if={ parent.opts.reply }>
+			<div class="ref" v-if="parent.opts.reply">
 				<mk-post-preview post={ parent.opts.reply }/>
 			</div>
 			<div class="body">
diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag
index d32a3b66f..e4a9800cf 100644
--- a/src/web/app/desktop/tags/post-form.tag
+++ b/src/web/app/desktop/tags/post-form.tag
@@ -10,7 +10,7 @@
 			</ul>
 			<p class="remain">{ 4 - files.length }/4</p>
 		</div>
-		<mk-poll-editor if={ poll } ref="poll" ondestroy={ onPollDestroyed }/>
+		<mk-poll-editor v-if="poll" ref="poll" ondestroy={ onPollDestroyed }/>
 	</div>
 	<mk-uploader ref="uploader"/>
 	<button ref="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" @click="selectFile">%fa:upload%</button>
@@ -19,10 +19,10 @@
 	<button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" @click="addPoll">%fa:chart-pie%</button>
 	<p class="text-count { over: refs.text.value.length > 1000 }">{ '%i18n:desktop.tags.mk-post-form.text-remain%'.replace('{}', 1000 - refs.text.value.length) }</p>
 	<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0 && files.length == 0 && !poll && !repost) } @click="post">
-		{ wait ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }<mk-ellipsis if={ wait }/>
+		{ wait ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }<mk-ellipsis v-if="wait"/>
 	</button>
 	<input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" onchange={ changeFile }/>
-	<div class="dropzone" if={ draghover }></div>
+	<div class="dropzone" v-if="draghover"></div>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/progress-dialog.tag b/src/web/app/desktop/tags/progress-dialog.tag
index 9f7df312e..2359802be 100644
--- a/src/web/app/desktop/tags/progress-dialog.tag
+++ b/src/web/app/desktop/tags/progress-dialog.tag
@@ -3,10 +3,10 @@
 		<yield to="header">{ parent.title }<mk-ellipsis/></yield>
 		<yield to="content">
 			<div class="body">
-				<p class="init" if={ isNaN(parent.value) }>待機中<mk-ellipsis/></p>
-				<p class="percentage" if={ !isNaN(parent.value) }>{ Math.floor((parent.value / parent.max) * 100) }</p>
-				<progress if={ !isNaN(parent.value) && parent.value < parent.max } value={ isNaN(parent.value) ? 0 : parent.value } max={ parent.max }></progress>
-				<div class="progress waiting" if={ parent.value >= parent.max }></div>
+				<p class="init" v-if="isNaN(parent.value)">待機中<mk-ellipsis/></p>
+				<p class="percentage" v-if="!isNaN(parent.value)">{ Math.floor((parent.value / parent.max) * 100) }</p>
+				<progress v-if="!isNaN(parent.value) && parent.value < parent.max" value={ isNaN(parent.value) ? 0 : parent.value } max={ parent.max }></progress>
+				<div class="progress waiting" v-if="parent.value >= parent.max"></div>
 			</div>
 		</yield>
 	</mk-window>
diff --git a/src/web/app/desktop/tags/repost-form.tag b/src/web/app/desktop/tags/repost-form.tag
index da8683ab6..06ee32150 100644
--- a/src/web/app/desktop/tags/repost-form.tag
+++ b/src/web/app/desktop/tags/repost-form.tag
@@ -1,13 +1,13 @@
 <mk-repost-form>
 	<mk-post-preview post={ opts.post }/>
-	<virtual if={ !quote }>
+	<virtual v-if="!quote">
 		<footer>
-			<a class="quote" if={ !quote } @click="onquote">%i18n:desktop.tags.mk-repost-form.quote%</a>
+			<a class="quote" v-if="!quote" @click="onquote">%i18n:desktop.tags.mk-repost-form.quote%</a>
 			<button class="cancel" @click="cancel">%i18n:desktop.tags.mk-repost-form.cancel%</button>
 			<button class="ok" @click="ok" disabled={ wait }>{ wait ? '%i18n:desktop.tags.mk-repost-form.reposting%' : '%i18n:desktop.tags.mk-repost-form.repost%' }</button>
 		</footer>
 	</virtual>
-	<virtual if={ quote }>
+	<virtual v-if="quote">
 		<mk-post-form ref="form" repost={ opts.post }/>
 	</virtual>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag
index d263f9576..3343697ca 100644
--- a/src/web/app/desktop/tags/search-posts.tag
+++ b/src/web/app/desktop/tags/search-posts.tag
@@ -1,12 +1,12 @@
 <mk-search-posts>
-	<div class="loading" if={ isLoading }>
+	<div class="loading" v-if="isLoading">
 		<mk-ellipsis-icon/>
 	</div>
-	<p class="empty" if={ isEmpty }>%fa:search%「{ query }」に関する投稿は見つかりませんでした。</p>
+	<p class="empty" v-if="isEmpty">%fa:search%「{ query }」に関する投稿は見つかりませんでした。</p>
 	<mk-timeline ref="timeline">
 		<yield to="footer">
-			<virtual if={ !parent.moreLoading }>%fa:moon%</virtual>
-			<virtual if={ parent.moreLoading }>%fa:spinner .pulse .fw%</virtual>
+			<virtual v-if="!parent.moreLoading">%fa:moon%</virtual>
+			<virtual v-if="parent.moreLoading">%fa:spinner .pulse .fw%</virtual>
 		</yield/>
 	</mk-timeline>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/select-file-from-drive-window.tag b/src/web/app/desktop/tags/select-file-from-drive-window.tag
index 8e9359b05..f776f0ecb 100644
--- a/src/web/app/desktop/tags/select-file-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-file-from-drive-window.tag
@@ -2,7 +2,7 @@
 	<mk-window ref="window" is-modal={ true } width={ '800px' } height={ '500px' }>
 		<yield to="header">
 			<mk-raw content={ parent.title }/>
-			<span class="count" if={ parent.multiple && parent.files.length > 0 }>({ parent.files.length }ファイル選択中)</span>
+			<span class="count" v-if="parent.multiple && parent.files.length > 0">({ parent.files.length }ファイル選択中)</span>
 		</yield>
 		<yield to="content">
 			<mk-drive-browser ref="browser" multiple={ parent.multiple }/>
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 211e36741..1e3097ba1 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -265,12 +265,12 @@
 <mk-2fa-setting>
 	<p>%i18n:desktop.tags.mk-2fa-setting.intro%<a href="%i18n:desktop.tags.mk-2fa-setting.url%" target="_blank">%i18n:desktop.tags.mk-2fa-setting.detail%</a></p>
 	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-2fa-setting.caution%</p></div>
-	<p if={ !data && !I.two_factor_enabled }><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
-	<virtual if={ I.two_factor_enabled }>
+	<p v-if="!data && !I.two_factor_enabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
+	<virtual v-if="I.two_factor_enabled">
 		<p>%i18n:desktop.tags.mk-2fa-setting.already-registered%</p>
 		<button @click="unregister" class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button>
 	</virtual>
-	<div if={ data }>
+	<div v-if="data">
 		<ol>
 			<li>%i18n:desktop.tags.mk-2fa-setting.authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:desktop.tags.mk-2fa-setting.howtoinstall%</a></li>
 			<li>%i18n:desktop.tags.mk-2fa-setting.scan%<br><img src={ data.qr }></li>
@@ -394,10 +394,10 @@
 </mk-drive-setting>
 
 <mk-mute-setting>
-	<div class="none ui info" if={ !fetching && users.length == 0 }>
+	<div class="none ui info" v-if="!fetching && users.length == 0">
 		<p>%fa:info-circle%%i18n:desktop.tags.mk-mute-setting.no-users%</p>
 	</div>
-	<div class="users" if={ users.length != 0 }>
+	<div class="users" v-if="users.length != 0">
 		<div each={ user in users }>
 			<p><b>{ user.name }</b> @{ user.username }</p>
 		</div>
diff --git a/src/web/app/desktop/tags/sub-post-content.tag b/src/web/app/desktop/tags/sub-post-content.tag
index a07180b67..184fc53eb 100644
--- a/src/web/app/desktop/tags/sub-post-content.tag
+++ b/src/web/app/desktop/tags/sub-post-content.tag
@@ -1,16 +1,16 @@
 <mk-sub-post-content>
 	<div class="body">
-		<a class="reply" if={ post.reply_id }>
+		<a class="reply" v-if="post.reply_id">
 			%fa:reply%
 		</a>
 		<span ref="text"></span>
-		<a class="quote" if={ post.repost_id } href={ '/post:' + post.repost_id }>RP: ...</a>
+		<a class="quote" v-if="post.repost_id" href={ '/post:' + post.repost_id }>RP: ...</a>
 	</div>
-	<details if={ post.media }>
+	<details v-if="post.media">
 		<summary>({ post.media.length }つのメディア)</summary>
 		<mk-images images={ post.media }/>
 	</details>
-	<details if={ post.poll }>
+	<details v-if="post.poll">
 		<summary>投票</summary>
 		<mk-poll post={ post }/>
 	</details>
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 008c69017..98970bfa1 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -1,7 +1,7 @@
 <mk-timeline>
 	<virtual each={ post, i in posts }>
 		<mk-timeline-post post={ post }/>
-		<p class="date" if={ i != posts.length - 1 && post._date != posts[i + 1]._date }><span>%fa:angle-up%{ post._datetext }</span><span>%fa:angle-down%{ posts[i + 1]._datetext }</span></p>
+		<p class="date" v-if="i != posts.length - 1 && post._date != posts[i + 1]._date"><span>%fa:angle-up%{ post._datetext }</span><span>%fa:angle-down%{ posts[i + 1]._datetext }</span></p>
 	</virtual>
 	<footer data-yield="footer">
 		<yield from="footer"/>
@@ -82,10 +82,10 @@
 </mk-timeline>
 
 <mk-timeline-post tabindex="-1" title={ title } onkeydown={ onKeyDown } dblclick={ onDblClick }>
-	<div class="reply-to" if={ p.reply }>
+	<div class="reply-to" v-if="p.reply">
 		<mk-timeline-post-sub post={ p.reply }/>
 	</div>
-	<div class="repost" if={ isRepost }>
+	<div class="repost" v-if="isRepost">
 		<p>
 			<a class="avatar-anchor" href={ '/' + post.user.username } data-user-preview={ post.user_id }>
 				<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=32' } alt="avatar"/>
@@ -101,10 +101,10 @@
 		<div class="main">
 			<header>
 				<a class="name" href={ '/' + p.user.username } data-user-preview={ p.user.id }>{ p.user.name }</a>
-				<span class="is-bot" if={ p.user.is_bot }>bot</span>
+				<span class="is-bot" v-if="p.user.is_bot">bot</span>
 				<span class="username">@{ p.user.username }</span>
 				<div class="info">
-					<span class="app" if={ p.app }>via <b>{ p.app.name }</b></span>
+					<span class="app" v-if="p.app">via <b>{ p.app.name }</b></span>
 					<a class="created-at" href={ url }>
 						<mk-time time={ p.created_at }/>
 					</a>
@@ -112,43 +112,43 @@
 			</header>
 			<div class="body">
 				<div class="text" ref="text">
-					<p class="channel" if={ p.channel != null }><a href={ _CH_URL_ + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
-					<a class="reply" if={ p.reply }>
+					<p class="channel" v-if="p.channel != null"><a href={ _CH_URL_ + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
+					<a class="reply" v-if="p.reply">
 						%fa:reply%
 					</a>
 					<p class="dummy"></p>
-					<a class="quote" if={ p.repost != null }>RP:</a>
+					<a class="quote" v-if="p.repost != null">RP:</a>
 				</div>
-				<div class="media" if={ p.media }>
+				<div class="media" v-if="p.media">
 					<mk-images images={ p.media }/>
 				</div>
-				<mk-poll if={ p.poll } post={ p } ref="pollViewer"/>
-				<div class="repost" if={ p.repost }>%fa:quote-right -flip-h%
+				<mk-poll v-if="p.poll" post={ p } ref="pollViewer"/>
+				<div class="repost" v-if="p.repost">%fa:quote-right -flip-h%
 					<mk-post-preview class="repost" post={ p.repost }/>
 				</div>
 			</div>
 			<footer>
 				<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
 				<button @click="reply" title="%i18n:desktop.tags.mk-timeline-post.reply%">
-					%fa:reply%<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
+					%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
 				</button>
 				<button @click="repost" title="%i18n:desktop.tags.mk-timeline-post.repost%">
-					%fa:retweet%<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
+					%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
 				</button>
 				<button class={ reacted: p.my_reaction != null } @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-post.add-reaction%">
-					%fa:plus%<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
+					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
 				</button>
 				<button @click="menu" ref="menuButton">
 					%fa:ellipsis-h%
 				</button>
 				<button @click="toggleDetail" title="%i18n:desktop.tags.mk-timeline-post.detail">
-					<virtual if={ !isDetailOpened }>%fa:caret-down%</virtual>
-					<virtual if={ isDetailOpened }>%fa:caret-up%</virtual>
+					<virtual v-if="!isDetailOpened">%fa:caret-down%</virtual>
+					<virtual v-if="isDetailOpened">%fa:caret-up%</virtual>
 				</button>
 			</footer>
 		</div>
 	</article>
-	<div class="detail" if={ isDetailOpened }>
+	<div class="detail" v-if="isDetailOpened">
 		<mk-post-status-graph width="462" height="130" post={ p }/>
 	</div>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index cae30dbe2..a8ddcaf93 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -1,11 +1,11 @@
 <mk-ui>
 	<mk-ui-header page={ opts.page }/>
-	<mk-set-avatar-suggestion if={ SIGNIN && I.avatar_id == null }/>
-	<mk-set-banner-suggestion if={ SIGNIN && I.banner_id == null }/>
+	<mk-set-avatar-suggestion v-if="SIGNIN && I.avatar_id == null"/>
+	<mk-set-banner-suggestion v-if="SIGNIN && I.banner_id == null"/>
 	<div class="content">
 		<yield />
 	</div>
-	<mk-stream-indicator if={ SIGNIN }/>
+	<mk-stream-indicator v-if="SIGNIN"/>
 	<style lang="stylus" scoped>
 		:scope
 			display block
@@ -37,7 +37,7 @@
 </mk-ui>
 
 <mk-ui-header>
-	<mk-donation if={ SIGNIN && I.client_settings.show_donation }/>
+	<mk-donation v-if="SIGNIN && I.client_settings.show_donation"/>
 	<mk-special-message/>
 	<div class="main">
 		<div class="backdrop"></div>
@@ -48,9 +48,9 @@
 				</div>
 				<div class="right">
 					<mk-ui-header-search/>
-					<mk-ui-header-account if={ SIGNIN }/>
-					<mk-ui-header-notifications if={ SIGNIN }/>
-					<mk-ui-header-post-button if={ SIGNIN }/>
+					<mk-ui-header-account v-if="SIGNIN"/>
+					<mk-ui-header-notifications v-if="SIGNIN"/>
+					<mk-ui-header-post-button v-if="SIGNIN"/>
 					<mk-ui-header-clock/>
 				</div>
 			</div>
@@ -230,9 +230,9 @@
 
 <mk-ui-header-notifications>
 	<button data-active={ isOpen } @click="toggle" title="%i18n:desktop.tags.mk-ui-header-notifications.title%">
-		%fa:R bell%<virtual if={ hasUnreadNotifications }>%fa:circle%</virtual>
+		%fa:R bell%<virtual v-if="hasUnreadNotifications">%fa:circle%</virtual>
 	</button>
-	<div class="notifications" if={ isOpen }>
+	<div class="notifications" v-if="isOpen">
 		<mk-notifications/>
 	</div>
 	<style lang="stylus" scoped>
@@ -392,7 +392,7 @@
 
 <mk-ui-header-nav>
 	<ul>
-		<virtual if={ SIGNIN }>
+		<virtual v-if="SIGNIN">
 			<li class="home { active: page == 'home' }">
 				<a href={ _URL_ }>
 					%fa:home%
@@ -403,7 +403,7 @@
 				<a @click="messaging">
 					%fa:comments%
 					<p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p>
-					<virtual if={ hasUnreadMessagingMessages }>%fa:circle%</virtual>
+					<virtual v-if="hasUnreadMessagingMessages">%fa:circle%</virtual>
 				</a>
 			</li>
 		</virtual>
@@ -630,10 +630,10 @@
 
 <mk-ui-header-account>
 	<button class="header" data-active={ isOpen.toString() } @click="toggle">
-		<span class="username">{ I.username }<virtual if={ !isOpen }>%fa:angle-down%</virtual><virtual if={ isOpen }>%fa:angle-up%</virtual></span>
+		<span class="username">{ I.username }<virtual v-if="!isOpen">%fa:angle-down%</virtual><virtual v-if="isOpen">%fa:angle-up%</virtual></span>
 		<img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 	</button>
-	<div class="menu" if={ isOpen }>
+	<div class="menu" v-if="isOpen">
 		<ul>
 			<li>
 				<a href={ '/' + I.username }>%fa:user%%i18n:desktop.tags.mk-ui-header-account.profile%%fa:angle-right%</a>
diff --git a/src/web/app/desktop/tags/user-preview.tag b/src/web/app/desktop/tags/user-preview.tag
index cf7b96275..eb3568ce0 100644
--- a/src/web/app/desktop/tags/user-preview.tag
+++ b/src/web/app/desktop/tags/user-preview.tag
@@ -1,5 +1,5 @@
 <mk-user-preview>
-	<virtual if={ user != null }>
+	<virtual v-if="user != null">
 		<div class="banner" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=512)' : '' }></div><a class="avatar" href={ '/' + user.username } target="_blank"><img src={ user.avatar_url + '?thumbnail&size=64' } alt="avatar"/></a>
 		<div class="title">
 			<p class="name">{ user.name }</p>
@@ -17,7 +17,7 @@
 				<p>フォロワー</p><a>{ user.followers_count }</a>
 			</div>
 		</div>
-		<mk-follow-button if={ SIGNIN && user.id != I.id } user={ userPromise }/>
+		<mk-follow-button v-if="SIGNIN && user.id != I.id" user={ userPromise }/>
 	</virtual>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/tags/user-timeline.tag
index be2649fb6..427ce9c53 100644
--- a/src/web/app/desktop/tags/user-timeline.tag
+++ b/src/web/app/desktop/tags/user-timeline.tag
@@ -2,14 +2,14 @@
 	<header>
 		<span data-is-active={ mode == 'default' } @click="setMode.bind(this, 'default')">投稿</span><span data-is-active={ mode == 'with-replies' } @click="setMode.bind(this, 'with-replies')">投稿と返信</span>
 	</header>
-	<div class="loading" if={ isLoading }>
+	<div class="loading" v-if="isLoading">
 		<mk-ellipsis-icon/>
 	</div>
-	<p class="empty" if={ isEmpty }>%fa:R comments%このユーザーはまだ何も投稿していないようです。</p>
+	<p class="empty" v-if="isEmpty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p>
 	<mk-timeline ref="timeline">
 		<yield to="footer">
-			<virtual if={ !parent.moreLoading }>%fa:moon%</virtual>
-			<virtual if={ parent.moreLoading }>%fa:spinner .pulse .fw%</virtual>
+			<virtual v-if="!parent.moreLoading">%fa:moon%</virtual>
+			<virtual v-if="parent.moreLoading">%fa:spinner .pulse .fw%</virtual>
 		</yield/>
 	</mk-timeline>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/tags/user.tag
index 046fef681..364b95ba7 100644
--- a/src/web/app/desktop/tags/user.tag
+++ b/src/web/app/desktop/tags/user.tag
@@ -1,10 +1,10 @@
 <mk-user>
-	<div class="user" if={ !fetching }>
+	<div class="user" v-if="!fetching">
 		<header>
 			<mk-user-header user={ user }/>
 		</header>
-		<mk-user-home if={ page == 'home' } user={ user }/>
-		<mk-user-graphs if={ page == 'graphs' } user={ user }/>
+		<mk-user-home v-if="page == 'home'" user={ user }/>
+		<mk-user-graphs v-if="page == 'graphs'" user={ user }/>
 	</div>
 	<style lang="stylus" scoped>
 		:scope
@@ -48,7 +48,7 @@
 		<div class="title">
 			<p class="name" href={ '/' + user.username }>{ user.name }</p>
 			<p class="username">@{ user.username }</p>
-			<p class="location" if={ user.profile.location }>%fa:map-marker%{ user.profile.location }</p>
+			<p class="location" v-if="user.profile.location">%fa:map-marker%{ user.profile.location }</p>
 		</div>
 		<footer>
 			<a href={ '/' + user.username } data-active={ parent.page == 'home' }>%fa:home%概要</a>
@@ -224,17 +224,17 @@
 </mk-user-header>
 
 <mk-user-profile>
-	<div class="friend-form" if={ SIGNIN && I.id != user.id }>
+	<div class="friend-form" v-if="SIGNIN && I.id != user.id">
 		<mk-big-follow-button user={ user }/>
-		<p class="followed" if={ user.is_followed }>%i18n:desktop.tags.mk-user.follows-you%</p>
-		<p if={ user.is_muted }>%i18n:desktop.tags.mk-user.muted% <a @click="unmute">%i18n:desktop.tags.mk-user.unmute%</a></p>
-		<p if={ !user.is_muted }><a @click="mute">%i18n:desktop.tags.mk-user.mute%</a></p>
+		<p class="followed" v-if="user.is_followed">%i18n:desktop.tags.mk-user.follows-you%</p>
+		<p v-if="user.is_muted">%i18n:desktop.tags.mk-user.muted% <a @click="unmute">%i18n:desktop.tags.mk-user.unmute%</a></p>
+		<p v-if="!user.is_muted"><a @click="mute">%i18n:desktop.tags.mk-user.mute%</a></p>
 	</div>
-	<div class="description" if={ user.description }>{ user.description }</div>
-	<div class="birthday" if={ user.profile.birthday }>
+	<div class="description" v-if="user.description">{ user.description }</div>
+	<div class="birthday" v-if="user.profile.birthday">
 		<p>%fa:birthday-cake%{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' } ({ age(user.profile.birthday) }歳)</p>
 	</div>
-	<div class="twitter" if={ user.twitter }>
+	<div class="twitter" v-if="user.twitter">
 		<p>%fa:B twitter%<a href={ 'https://twitter.com/' + user.twitter.screen_name } target="_blank">@{ user.twitter.screen_name }</a></p>
 	</div>
 	<div class="status">
@@ -355,13 +355,13 @@
 
 <mk-user-photos>
 	<p class="title">%fa:camera%%i18n:desktop.tags.mk-user.photos.title%</p>
-	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.photos.loading%<mk-ellipsis/></p>
-	<div class="stream" if={ !initializing && images.length > 0 }>
+	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.photos.loading%<mk-ellipsis/></p>
+	<div class="stream" v-if="!initializing && images.length > 0">
 		<virtual each={ image in images }>
 			<div class="img" style={ 'background-image: url(' + image.url + '?thumbnail&size=256)' }></div>
 		</virtual>
 	</div>
-	<p class="empty" if={ !initializing && images.length == 0 }>%i18n:desktop.tags.mk-user.photos.no-photos%</p>
+	<p class="empty" v-if="!initializing && images.length == 0">%i18n:desktop.tags.mk-user.photos.no-photos%</p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
@@ -449,8 +449,8 @@
 
 <mk-user-frequently-replied-users>
 	<p class="title">%fa:users%%i18n:desktop.tags.mk-user.frequently-replied-users.title%</p>
-	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p>
-	<div class="user" if={ !initializing && users.length != 0 } each={ _user in users }>
+	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p>
+	<div class="user" v-if="!initializing && users.length != 0" each={ _user in users }>
 		<a class="avatar-anchor" href={ '/' + _user.username }>
 			<img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ _user.id }/>
 		</a>
@@ -460,7 +460,7 @@
 		</div>
 		<mk-follow-button user={ _user }/>
 	</div>
-	<p class="empty" if={ !initializing && users.length == 0 }>%i18n:desktop.tags.mk-user.frequently-replied-users.no-users%</p>
+	<p class="empty" v-if="!initializing && users.length == 0">%i18n:desktop.tags.mk-user.frequently-replied-users.no-users%</p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
@@ -561,13 +561,13 @@
 
 <mk-user-followers-you-know>
 	<p class="title">%fa:users%%i18n:desktop.tags.mk-user.followers-you-know.title%</p>
-	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p>
-	<div if={ !initializing && users.length > 0 }>
+	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p>
+	<div v-if="!initializing && users.length > 0">
 	<virtual each={ user in users }>
 		<a href={ '/' + user.username }><img src={ user.avatar_url + '?thumbnail&size=64' } alt={ user.name }/></a>
 	</virtual>
 	</div>
-	<p class="empty" if={ !initializing && users.length == 0 }>%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
+	<p class="empty" v-if="!initializing && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
@@ -638,12 +638,12 @@
 		<div ref="left">
 			<mk-user-profile user={ user }/>
 			<mk-user-photos user={ user }/>
-			<mk-user-followers-you-know if={ SIGNIN && I.id !== user.id } user={ user }/>
+			<mk-user-followers-you-know v-if="SIGNIN && I.id !== user.id" user={ user }/>
 			<p>%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time time={ user.last_used_at }/></b></p>
 		</div>
 	</div>
 	<main>
-		<mk-post-detail if={ user.pinned_post } post={ user.pinned_post } compact={ true }/>
+		<mk-post-detail v-if="user.pinned_post" post={ user.pinned_post } compact={ true }/>
 		<mk-user-timeline ref="tl" user={ user }/>
 	</main>
 	<div>
@@ -784,7 +784,7 @@
 </mk-user-graphs>
 
 <mk-user-graphs-activity-chart>
-	<svg if={ data } ref="canvas" viewBox="0 0 365 1" preserveAspectRatio="none">
+	<svg v-if="data" ref="canvas" viewBox="0 0 365 1" preserveAspectRatio="none">
 		<g each={ d, i in data.reverse() }>
 			<rect width="0.8" riot-height={ d.postsH }
 				riot-x={ i + 0.1 } riot-y={ 1 - d.postsH - d.repliesH - d.repostsH }
diff --git a/src/web/app/desktop/tags/users-list.tag b/src/web/app/desktop/tags/users-list.tag
index fd5c73b7d..18ba2b77d 100644
--- a/src/web/app/desktop/tags/users-list.tag
+++ b/src/web/app/desktop/tags/users-list.tag
@@ -2,20 +2,20 @@
 	<nav>
 		<div>
 			<span data-is-active={ mode == 'all' } @click="setMode.bind(this, 'all')">すべて<span>{ opts.count }</span></span>
-			<span if={ SIGNIN && opts.youKnowCount } data-is-active={ mode == 'iknow' } @click="setMode.bind(this, 'iknow')">知り合い<span>{ opts.youKnowCount }</span></span>
+			<span v-if="SIGNIN && opts.youKnowCount" data-is-active={ mode == 'iknow' } @click="setMode.bind(this, 'iknow')">知り合い<span>{ opts.youKnowCount }</span></span>
 		</div>
 	</nav>
-	<div class="users" if={ !fetching && users.length != 0 }>
+	<div class="users" v-if="!fetching && users.length != 0">
 		<div each={ users }>
 			<mk-list-user user={ this }/>
 		</div>
 	</div>
-	<button class="more" if={ !fetching && next != null } @click="more" disabled={ moreFetching }>
-		<span if={ !moreFetching }>もっと</span>
-		<span if={ moreFetching }>読み込み中<mk-ellipsis/></span>
+	<button class="more" v-if="!fetching && next != null" @click="more" disabled={ moreFetching }>
+		<span v-if="!moreFetching">もっと</span>
+		<span v-if="moreFetching">読み込み中<mk-ellipsis/></span>
 	</button>
-	<p class="no" if={ !fetching && users.length == 0 }>{ opts.noUsers }</p>
-	<p class="fetching" if={ fetching }>%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
+	<p class="no" v-if="!fetching && users.length == 0">{ opts.noUsers }</p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/widgets/activity.tag b/src/web/app/desktop/tags/widgets/activity.tag
index b9132e5a5..8aad5337f 100644
--- a/src/web/app/desktop/tags/widgets/activity.tag
+++ b/src/web/app/desktop/tags/widgets/activity.tag
@@ -1,11 +1,11 @@
 <mk-activity-widget data-melt={ design == 2 }>
-	<virtual if={ design == 0 }>
+	<virtual v-if="design == 0">
 		<p class="title">%fa:chart-bar%%i18n:desktop.tags.mk-activity-widget.title%</p>
 		<button @click="toggle" title="%i18n:desktop.tags.mk-activity-widget.toggle%">%fa:sort%</button>
 	</virtual>
-	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<mk-activity-widget-calender if={ !initializing && view == 0 } data={ [].concat(activity) }/>
-	<mk-activity-widget-chart if={ !initializing && view == 1 } data={ [].concat(activity) }/>
+	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<mk-activity-widget-calender v-if="!initializing && view == 0" data={ [].concat(activity) }/>
+	<mk-activity-widget-chart v-if="!initializing && view == 1" data={ [].concat(activity) }/>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/widgets/calendar.tag b/src/web/app/desktop/tags/widgets/calendar.tag
index 4e365650c..c8d268783 100644
--- a/src/web/app/desktop/tags/widgets/calendar.tag
+++ b/src/web/app/desktop/tags/widgets/calendar.tag
@@ -1,12 +1,12 @@
 <mk-calendar-widget data-melt={ opts.design == 4 || opts.design == 5 }>
-	<virtual if={ opts.design == 0 || opts.design == 1 }>
+	<virtual v-if="opts.design == 0 || opts.design == 1">
 		<button @click="prev" title="%i18n:desktop.tags.mk-calendar-widget.prev%">%fa:chevron-circle-left%</button>
 		<p class="title">{ '%i18n:desktop.tags.mk-calendar-widget.title%'.replace('{1}', year).replace('{2}', month) }</p>
 		<button @click="next" title="%i18n:desktop.tags.mk-calendar-widget.next%">%fa:chevron-circle-right%</button>
 	</virtual>
 
 	<div class="calendar">
-		<div class="weekday" if={ opts.design == 0 || opts.design == 2 || opts.design == 4} each={ day, i in Array(7).fill(0) }
+		<div class="weekday" v-if="opts.design == 0 || opts.design == 2 || opts.design == 4} each={ day, i in Array(7).fill(0)"
 			data-today={ year == today.getFullYear() && month == today.getMonth() + 1 && today.getDay() == i }
 			data-is-donichi={ i == 0 || i == 6 }>{ weekdayText[i] }</div>
 		<div each={ day, i in Array(paddingDays).fill(0) }></div>
diff --git a/src/web/app/desktop/tags/window.tag b/src/web/app/desktop/tags/window.tag
index 3752f3609..2b98ab7f0 100644
--- a/src/web/app/desktop/tags/window.tag
+++ b/src/web/app/desktop/tags/window.tag
@@ -5,20 +5,20 @@
 			<header ref="header" onmousedown={ onHeaderMousedown }>
 				<h1 data-yield="header"><yield from="header"/></h1>
 				<div>
-					<button class="popout" if={ popoutUrl } onmousedown={ repelMove } @click="popout" title="ポップアウト">%fa:R window-restore%</button>
-					<button class="close" if={ canClose } onmousedown={ repelMove } @click="close" title="閉じる">%fa:times%</button>
+					<button class="popout" v-if="popoutUrl" onmousedown={ repelMove } @click="popout" title="ポップアウト">%fa:R window-restore%</button>
+					<button class="close" v-if="canClose" onmousedown={ repelMove } @click="close" title="閉じる">%fa:times%</button>
 				</div>
 			</header>
 			<div class="content" data-yield="content"><yield from="content"/></div>
 		</div>
-		<div class="handle top" if={ canResize } onmousedown={ onTopHandleMousedown }></div>
-		<div class="handle right" if={ canResize } onmousedown={ onRightHandleMousedown }></div>
-		<div class="handle bottom" if={ canResize } onmousedown={ onBottomHandleMousedown }></div>
-		<div class="handle left" if={ canResize } onmousedown={ onLeftHandleMousedown }></div>
-		<div class="handle top-left" if={ canResize } onmousedown={ onTopLeftHandleMousedown }></div>
-		<div class="handle top-right" if={ canResize } onmousedown={ onTopRightHandleMousedown }></div>
-		<div class="handle bottom-right" if={ canResize } onmousedown={ onBottomRightHandleMousedown }></div>
-		<div class="handle bottom-left" if={ canResize } onmousedown={ onBottomLeftHandleMousedown }></div>
+		<div class="handle top" v-if="canResize" onmousedown={ onTopHandleMousedown }></div>
+		<div class="handle right" v-if="canResize" onmousedown={ onRightHandleMousedown }></div>
+		<div class="handle bottom" v-if="canResize" onmousedown={ onBottomHandleMousedown }></div>
+		<div class="handle left" v-if="canResize" onmousedown={ onLeftHandleMousedown }></div>
+		<div class="handle top-left" v-if="canResize" onmousedown={ onTopLeftHandleMousedown }></div>
+		<div class="handle top-right" v-if="canResize" onmousedown={ onTopRightHandleMousedown }></div>
+		<div class="handle bottom-right" v-if="canResize" onmousedown={ onBottomRightHandleMousedown }></div>
+		<div class="handle bottom-left" v-if="canResize" onmousedown={ onBottomLeftHandleMousedown }></div>
 	</div>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/dev/tags/new-app-form.tag b/src/web/app/dev/tags/new-app-form.tag
index 1bd5b5a83..f753b5ae3 100644
--- a/src/web/app/dev/tags/new-app-form.tag
+++ b/src/web/app/dev/tags/new-app-form.tag
@@ -10,13 +10,13 @@
 			<label>
 				<p class="caption">Named ID</p>
 				<input ref="nid" type="text" pattern="^[a-zA-Z0-9-]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required="required" onkeyup={ onChangeNid }/>
-				<p class="info" if={ nidState == 'wait' } style="color:#999">%fa:spinner .pulse .fw%確認しています...</p>
-				<p class="info" if={ nidState == 'ok' } style="color:#3CB7B5">%fa:fw check%利用できます</p>
-				<p class="info" if={ nidState == 'unavailable' } style="color:#FF1161">%fa:fw exclamation-triangle%既に利用されています</p>
-				<p class="info" if={ nidState == 'error' } style="color:#FF1161">%fa:fw exclamation-triangle%通信エラー</p>
-				<p class="info" if={ nidState == 'invalid-format' } style="color:#FF1161">%fa:fw exclamation-triangle%a~z、A~Z、0~9、-(ハイフン)が使えます</p>
-				<p class="info" if={ nidState == 'min-range' } style="color:#FF1161">%fa:fw exclamation-triangle%3文字以上でお願いします!</p>
-				<p class="info" if={ nidState == 'max-range' } style="color:#FF1161">%fa:fw exclamation-triangle%30文字以内でお願いします</p>
+				<p class="info" v-if="nidState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%確認しています...</p>
+				<p class="info" v-if="nidState == 'ok'" style="color:#3CB7B5">%fa:fw check%利用できます</p>
+				<p class="info" v-if="nidState == 'unavailable'" style="color:#FF1161">%fa:fw exclamation-triangle%既に利用されています</p>
+				<p class="info" v-if="nidState == 'error'" style="color:#FF1161">%fa:fw exclamation-triangle%通信エラー</p>
+				<p class="info" v-if="nidState == 'invalid-format'" style="color:#FF1161">%fa:fw exclamation-triangle%a~z、A~Z、0~9、-(ハイフン)が使えます</p>
+				<p class="info" v-if="nidState == 'min-range'" style="color:#FF1161">%fa:fw exclamation-triangle%3文字以上でお願いします!</p>
+				<p class="info" v-if="nidState == 'max-range'" style="color:#FF1161">%fa:fw exclamation-triangle%30文字以内でお願いします</p>
 			</label>
 		</section>
 		<section class="description">
diff --git a/src/web/app/dev/tags/pages/app.tag b/src/web/app/dev/tags/pages/app.tag
index 3fdf8d15b..1e89b47d8 100644
--- a/src/web/app/dev/tags/pages/app.tag
+++ b/src/web/app/dev/tags/pages/app.tag
@@ -1,6 +1,6 @@
 <mk-app-page>
-	<p if={ fetching }>読み込み中</p>
-	<main if={ !fetching }>
+	<p v-if="fetching">読み込み中</p>
+	<main v-if="!fetching">
 		<header>
 			<h1>{ app.name }</h1>
 		</header>
diff --git a/src/web/app/dev/tags/pages/apps.tag b/src/web/app/dev/tags/pages/apps.tag
index fbacee137..d11011ca4 100644
--- a/src/web/app/dev/tags/pages/apps.tag
+++ b/src/web/app/dev/tags/pages/apps.tag
@@ -1,10 +1,10 @@
 <mk-apps-page>
 	<h1>アプリを管理</h1><a href="/app/new">アプリ作成</a>
 	<div class="apps">
-		<p if={ fetching }>読み込み中</p>
-		<virtual if={ !fetching }>
-			<p if={ apps.length == 0 }>アプリなし</p>
-			<ul if={ apps.length > 0 }>
+		<p v-if="fetching">読み込み中</p>
+		<virtual v-if="!fetching">
+			<p v-if="apps.length == 0">アプリなし</p>
+			<ul v-if="apps.length > 0">
 				<li each={ app in apps }><a href={ '/app/' + app.id }>
 						<p class="name">{ app.name }</p></a></li>
 			</ul>
diff --git a/src/web/app/mobile/tags/drive-selector.tag b/src/web/app/mobile/tags/drive-selector.tag
index 9c3a4b5c4..a837f8b5f 100644
--- a/src/web/app/mobile/tags/drive-selector.tag
+++ b/src/web/app/mobile/tags/drive-selector.tag
@@ -1,9 +1,9 @@
 <mk-drive-selector>
 	<div class="body">
 		<header>
-			<h1>%i18n:mobile.tags.mk-drive-selector.select-file%<span class="count" if={ files.length > 0 }>({ files.length })</span></h1>
+			<h1>%i18n:mobile.tags.mk-drive-selector.select-file%<span class="count" v-if="files.length > 0">({ files.length })</span></h1>
 			<button class="close" @click="cancel">%fa:times%</button>
-			<button if={ opts.multiple } class="ok" @click="ok">%fa:check%</button>
+			<button v-if="opts.multiple" class="ok" @click="ok">%fa:check%</button>
 		</header>
 		<mk-drive ref="browser" select-file={ true } multiple={ opts.multiple }/>
 	</div>
diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index a063d0ca6..0076dc8f4 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -5,52 +5,52 @@
 			<span>%fa:angle-right%</span>
 			<a @click="move" href="/i/drive/folder/{ folder.id }">{ folder.name }</a>
 		</virtual>
-		<virtual if={ folder != null }>
+		<virtual v-if="folder != null">
 			<span>%fa:angle-right%</span>
 			<p>{ folder.name }</p>
 		</virtual>
-		<virtual if={ file != null }>
+		<virtual v-if="file != null">
 			<span>%fa:angle-right%</span>
 			<p>{ file.name }</p>
 		</virtual>
 	</nav>
 	<mk-uploader ref="uploader"/>
-	<div class="browser { fetching: fetching }" if={ file == null }>
-		<div class="info" if={ info }>
-			<p if={ folder == null }>{ (info.usage / info.capacity * 100).toFixed(1) }% %i18n:mobile.tags.mk-drive.used%</p>
-			<p if={ folder != null && (folder.folders_count > 0 || folder.files_count > 0) }>
-				<virtual if={ folder.folders_count > 0 }>{ folder.folders_count } %i18n:mobile.tags.mk-drive.folder-count%</virtual>
-				<virtual if={ folder.folders_count > 0 && folder.files_count > 0 }>%i18n:mobile.tags.mk-drive.count-separator%</virtual>
-				<virtual if={ folder.files_count > 0 }>{ folder.files_count } %i18n:mobile.tags.mk-drive.file-count%</virtual>
+	<div class="browser { fetching: fetching }" v-if="file == null">
+		<div class="info" v-if="info">
+			<p v-if="folder == null">{ (info.usage / info.capacity * 100).toFixed(1) }% %i18n:mobile.tags.mk-drive.used%</p>
+			<p v-if="folder != null && (folder.folders_count > 0 || folder.files_count > 0)">
+				<virtual v-if="folder.folders_count > 0">{ folder.folders_count } %i18n:mobile.tags.mk-drive.folder-count%</virtual>
+				<virtual v-if="folder.folders_count > 0 && folder.files_count > 0">%i18n:mobile.tags.mk-drive.count-separator%</virtual>
+				<virtual v-if="folder.files_count > 0">{ folder.files_count } %i18n:mobile.tags.mk-drive.file-count%</virtual>
 			</p>
 		</div>
-		<div class="folders" if={ folders.length > 0 }>
+		<div class="folders" v-if="folders.length > 0">
 			<virtual each={ folder in folders }>
 				<mk-drive-folder folder={ folder }/>
 			</virtual>
-			<p if={ moreFolders }>%i18n:mobile.tags.mk-drive.load-more%</p>
+			<p v-if="moreFolders">%i18n:mobile.tags.mk-drive.load-more%</p>
 		</div>
-		<div class="files" if={ files.length > 0 }>
+		<div class="files" v-if="files.length > 0">
 			<virtual each={ file in files }>
 				<mk-drive-file file={ file }/>
 			</virtual>
-			<button class="more" if={ moreFiles } @click="fetchMoreFiles">
+			<button class="more" v-if="moreFiles" @click="fetchMoreFiles">
 				{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-drive.load-more%' }
 			</button>
 		</div>
-		<div class="empty" if={ files.length == 0 && folders.length == 0 && !fetching }>
-			<p if={ folder == null }>%i18n:mobile.tags.mk-drive.nothing-in-drive%</p>
-			<p if={ folder != null }>%i18n:mobile.tags.mk-drive.folder-is-empty%</p>
+		<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
+			<p v-if="folder == null">%i18n:mobile.tags.mk-drive.nothing-in-drive%</p>
+			<p v-if="folder != null">%i18n:mobile.tags.mk-drive.folder-is-empty%</p>
 		</div>
 	</div>
-	<div class="fetching" if={ fetching && file == null && files.length == 0 && folders.length == 0 }>
+	<div class="fetching" v-if="fetching && file == null && files.length == 0 && folders.length == 0">
 		<div class="spinner">
 			<div class="dot1"></div>
 			<div class="dot2"></div>
 		</div>
 	</div>
 	<input ref="file" type="file" multiple="multiple" onchange={ changeLocalFile }/>
-	<mk-drive-file-viewer if={ file != null } file={ file }/>
+	<mk-drive-file-viewer v-if="file != null" file={ file }/>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag
index 119ad1fb2..5d06507c4 100644
--- a/src/web/app/mobile/tags/drive/file-viewer.tag
+++ b/src/web/app/mobile/tags/drive/file-viewer.tag
@@ -1,13 +1,13 @@
 <mk-drive-file-viewer>
 	<div class="preview">
-		<img if={ kind == 'image' } ref="img"
+		<img v-if="kind == 'image'" ref="img"
 			src={ file.url }
 			alt={ file.name }
 			title={ file.name }
 			onload={ onImageLoaded }
 			style="background-color:rgb({ file.properties.average_color.join(',') })">
-		<virtual if={ kind != 'image' }>%fa:file%</virtual>
-		<footer if={ kind == 'image' && file.properties && file.properties.width && file.properties.height }>
+		<virtual v-if="kind != 'image'">%fa:file%</virtual>
+		<footer v-if="kind == 'image' && file.properties && file.properties.width && file.properties.height">
 			<span class="size">
 				<span class="width">{ file.properties.width }</span>
 				<span class="time">×</span>
diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag
index 96754e1b3..03cbab2bf 100644
--- a/src/web/app/mobile/tags/drive/file.tag
+++ b/src/web/app/mobile/tags/drive/file.tag
@@ -3,7 +3,7 @@
 		<div class="container">
 			<div class="thumbnail" style={ thumbnail }></div>
 			<div class="body">
-				<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
+				<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" v-if="file.name.lastIndexOf('.') != -1">{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
 				<!--
 				if file.tags.length > 0
 					ul.tags
diff --git a/src/web/app/mobile/tags/follow-button.tag b/src/web/app/mobile/tags/follow-button.tag
index baf8f2ffa..d96389bfc 100644
--- a/src/web/app/mobile/tags/follow-button.tag
+++ b/src/web/app/mobile/tags/follow-button.tag
@@ -1,10 +1,10 @@
 <mk-follow-button>
-	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } if={ !init } @click="onclick" disabled={ wait }>
-		<virtual if={ !wait && user.is_following }>%fa:minus%</virtual>
-		<virtual if={ !wait && !user.is_following }>%fa:plus%</virtual>
-		<virtual if={ wait }>%fa:spinner .pulse .fw%</virtual>{ user.is_following ? '%i18n:mobile.tags.mk-follow-button.unfollow%' : '%i18n:mobile.tags.mk-follow-button.follow%' }
+	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } v-if="!init" @click="onclick" disabled={ wait }>
+		<virtual v-if="!wait && user.is_following">%fa:minus%</virtual>
+		<virtual v-if="!wait && !user.is_following">%fa:plus%</virtual>
+		<virtual v-if="wait">%fa:spinner .pulse .fw%</virtual>{ user.is_following ? '%i18n:mobile.tags.mk-follow-button.unfollow%' : '%i18n:mobile.tags.mk-follow-button.follow%' }
 	</button>
-	<div class="init" if={ init }>%fa:spinner .pulse .fw%</div>
+	<div class="init" v-if="init">%fa:spinner .pulse .fw%</div>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag
index 86708bfee..3905e867b 100644
--- a/src/web/app/mobile/tags/home-timeline.tag
+++ b/src/web/app/mobile/tags/home-timeline.tag
@@ -1,5 +1,5 @@
 <mk-home-timeline>
-	<mk-init-following if={ noFollowing } />
+	<mk-init-following v-if="noFollowing" />
 	<mk-timeline ref="timeline" init={ init } more={ more } empty={ '%i18n:mobile.tags.mk-home-timeline.empty-timeline%' }/>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/mobile/tags/init-following.tag b/src/web/app/mobile/tags/init-following.tag
index e0e2532af..3eb3e1481 100644
--- a/src/web/app/mobile/tags/init-following.tag
+++ b/src/web/app/mobile/tags/init-following.tag
@@ -1,12 +1,12 @@
 <mk-init-following>
 	<p class="title">気になるユーザーをフォロー:</p>
-	<div class="users" if={ !fetching && users.length > 0 }>
+	<div class="users" v-if="!fetching && users.length > 0">
 		<virtual each={ users }>
 			<mk-user-card user={ this } />
 		</virtual>
 	</div>
-	<p class="empty" if={ !fetching && users.length == 0 }>おすすめのユーザーは見つかりませんでした。</p>
-	<p class="fetching" if={ fetching }>%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
+	<p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
 	<a class="refresh" @click="refresh">もっと見る</a>
 	<button class="close" @click="close" title="閉じる">%fa:times%</button>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/mobile/tags/notification-preview.tag b/src/web/app/mobile/tags/notification-preview.tag
index b2064cd42..a24110086 100644
--- a/src/web/app/mobile/tags/notification-preview.tag
+++ b/src/web/app/mobile/tags/notification-preview.tag
@@ -1,46 +1,46 @@
 <mk-notification-preview class={ notification.type }>
-	<virtual if={ notification.type == 'reaction' }>
+	<virtual v-if="notification.type == 'reaction'">
 		<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
 			<p><mk-reaction-icon reaction={ notification.reaction }/>{ notification.user.name }</p>
 			<p class="post-ref">%fa:quote-left%{ getPostSummary(notification.post) }%fa:quote-right%</p>
 		</div>
 	</virtual>
-	<virtual if={ notification.type == 'repost' }>
+	<virtual v-if="notification.type == 'repost'">
 		<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
 			<p>%fa:retweet%{ notification.post.user.name }</p>
 			<p class="post-ref">%fa:quote-left%{ getPostSummary(notification.post.repost) }%fa:quote-right%</p>
 		</div>
 	</virtual>
-	<virtual if={ notification.type == 'quote' }>
+	<virtual v-if="notification.type == 'quote'">
 		<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
 			<p>%fa:quote-left%{ notification.post.user.name }</p>
 			<p class="post-preview">{ getPostSummary(notification.post) }</p>
 		</div>
 	</virtual>
-	<virtual if={ notification.type == 'follow' }>
+	<virtual v-if="notification.type == 'follow'">
 		<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
 			<p>%fa:user-plus%{ notification.user.name }</p>
 		</div>
 	</virtual>
-	<virtual if={ notification.type == 'reply' }>
+	<virtual v-if="notification.type == 'reply'">
 		<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
 			<p>%fa:reply%{ notification.post.user.name }</p>
 			<p class="post-preview">{ getPostSummary(notification.post) }</p>
 		</div>
 	</virtual>
-	<virtual if={ notification.type == 'mention' }>
+	<virtual v-if="notification.type == 'mention'">
 		<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
 			<p>%fa:at%{ notification.post.user.name }</p>
 			<p class="post-preview">{ getPostSummary(notification.post) }</p>
 		</div>
 	</virtual>
-	<virtual if={ notification.type == 'poll_vote' }>
+	<virtual v-if="notification.type == 'poll_vote'">
 		<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
 			<p>%fa:chart-pie%{ notification.user.name }</p>
diff --git a/src/web/app/mobile/tags/notification.tag b/src/web/app/mobile/tags/notification.tag
index 23a9f2fe3..977244e0c 100644
--- a/src/web/app/mobile/tags/notification.tag
+++ b/src/web/app/mobile/tags/notification.tag
@@ -1,6 +1,6 @@
 <mk-notification class={ notification.type }>
 	<mk-time time={ notification.created_at }/>
-	<virtual if={ notification.type == 'reaction' }>
+	<virtual v-if="notification.type == 'reaction'">
 		<a class="avatar-anchor" href={ '/' + notification.user.username }>
 			<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		</a>
@@ -14,7 +14,7 @@
 			</a>
 		</div>
 	</virtual>
-	<virtual if={ notification.type == 'repost' }>
+	<virtual v-if="notification.type == 'repost'">
 		<a class="avatar-anchor" href={ '/' + notification.post.user.username }>
 			<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		</a>
@@ -28,7 +28,7 @@
 			</a>
 		</div>
 	</virtual>
-	<virtual if={ notification.type == 'quote' }>
+	<virtual v-if="notification.type == 'quote'">
 		<a class="avatar-anchor" href={ '/' + notification.post.user.username }>
 			<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		</a>
@@ -40,7 +40,7 @@
 			<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
 		</div>
 	</virtual>
-	<virtual if={ notification.type == 'follow' }>
+	<virtual v-if="notification.type == 'follow'">
 		<a class="avatar-anchor" href={ '/' + notification.user.username }>
 			<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		</a>
@@ -51,7 +51,7 @@
 			</p>
 		</div>
 	</virtual>
-	<virtual if={ notification.type == 'reply' }>
+	<virtual v-if="notification.type == 'reply'">
 		<a class="avatar-anchor" href={ '/' + notification.post.user.username }>
 			<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		</a>
@@ -63,7 +63,7 @@
 			<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
 		</div>
 	</virtual>
-	<virtual if={ notification.type == 'mention' }>
+	<virtual v-if="notification.type == 'mention'">
 		<a class="avatar-anchor" href={ '/' + notification.post.user.username }>
 			<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		</a>
@@ -75,7 +75,7 @@
 			<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
 		</div>
 	</virtual>
-	<virtual if={ notification.type == 'poll_vote' }>
+	<virtual v-if="notification.type == 'poll_vote'">
 		<a class="avatar-anchor" href={ '/' + notification.user.username }>
 			<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		</a>
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index ade71ea40..d1a6a2501 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -1,15 +1,15 @@
 <mk-notifications>
-	<div class="notifications" if={ notifications.length != 0 }>
+	<div class="notifications" v-if="notifications.length != 0">
 		<virtual each={ notification, i in notifications }>
 			<mk-notification notification={ notification }/>
-			<p class="date" if={ i != notifications.length - 1 && notification._date != notifications[i + 1]._date }><span>%fa:angle-up%{ notification._datetext }</span><span>%fa:angle-down%{ notifications[i + 1]._datetext }</span></p>
+			<p class="date" v-if="i != notifications.length - 1 && notification._date != notifications[i + 1]._date"><span>%fa:angle-up%{ notification._datetext }</span><span>%fa:angle-down%{ notifications[i + 1]._datetext }</span></p>
 		</virtual>
 	</div>
-	<button class="more" if={ moreNotifications } @click="fetchMoreNotifications" disabled={ fetchingMoreNotifications }>
-		<virtual if={ fetchingMoreNotifications }>%fa:spinner .pulse .fw%</virtual>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-notifications.more%' }
+	<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" disabled={ fetchingMoreNotifications }>
+		<virtual v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</virtual>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-notifications.more%' }
 	</button>
-	<p class="empty" if={ notifications.length == 0 && !loading }>%i18n:mobile.tags.mk-notifications.empty%</p>
-	<p class="loading" if={ loading }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<p class="empty" v-if="notifications.length == 0 && !loading">%i18n:mobile.tags.mk-notifications.empty%</p>
+	<p class="loading" v-if="loading">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/mobile/tags/page/entrance.tag b/src/web/app/mobile/tags/page/entrance.tag
index ebcf30f80..b244310cf 100644
--- a/src/web/app/mobile/tags/page/entrance.tag
+++ b/src/web/app/mobile/tags/page/entrance.tag
@@ -1,8 +1,8 @@
 <mk-entrance>
 	<main><img src="/assets/title.svg" alt="Misskey"/>
-		<mk-entrance-signin if={ mode == 'signin' }/>
-		<mk-entrance-signup if={ mode == 'signup' }/>
-		<div class="introduction" if={ mode == 'introduction' }>
+		<mk-entrance-signin v-if="mode == 'signin'"/>
+		<mk-entrance-signup v-if="mode == 'signup'"/>
+		<div class="introduction" v-if="mode == 'introduction'">
 			<mk-introduction/>
 			<button @click="signin">%i18n:common.ok%</button>
 		</div>
diff --git a/src/web/app/mobile/tags/page/messaging-room.tag b/src/web/app/mobile/tags/page/messaging-room.tag
index 075ea8e83..4a1c57b99 100644
--- a/src/web/app/mobile/tags/page/messaging-room.tag
+++ b/src/web/app/mobile/tags/page/messaging-room.tag
@@ -1,6 +1,6 @@
 <mk-messaging-room-page>
 	<mk-ui ref="ui">
-		<mk-messaging-room if={ !parent.fetching } user={ parent.user } is-naked={ true }/>
+		<mk-messaging-room v-if="!parent.fetching" user={ parent.user } is-naked={ true }/>
 	</mk-ui>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/mobile/tags/page/post.tag b/src/web/app/mobile/tags/page/post.tag
index 003f9dea5..296ef140c 100644
--- a/src/web/app/mobile/tags/page/post.tag
+++ b/src/web/app/mobile/tags/page/post.tag
@@ -1,11 +1,11 @@
 <mk-post-page>
 	<mk-ui ref="ui">
-		<main if={ !parent.fetching }>
-			<a if={ parent.post.next } href={ parent.post.next }>%fa:angle-up%%i18n:mobile.tags.mk-post-page.next%</a>
+		<main v-if="!parent.fetching">
+			<a v-if="parent.post.next" href={ parent.post.next }>%fa:angle-up%%i18n:mobile.tags.mk-post-page.next%</a>
 			<div>
 				<mk-post-detail ref="post" post={ parent.post }/>
 			</div>
-			<a if={ parent.post.prev } href={ parent.post.prev }>%fa:angle-down%%i18n:mobile.tags.mk-post-page.prev%</a>
+			<a v-if="parent.post.prev" href={ parent.post.prev }>%fa:angle-down%%i18n:mobile.tags.mk-post-page.prev%</a>
 		</main>
 	</mk-ui>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/mobile/tags/page/selectdrive.tag b/src/web/app/mobile/tags/page/selectdrive.tag
index c7ff66d05..ff11bad7d 100644
--- a/src/web/app/mobile/tags/page/selectdrive.tag
+++ b/src/web/app/mobile/tags/page/selectdrive.tag
@@ -1,8 +1,8 @@
 <mk-selectdrive-page>
 	<header>
-		<h1>%i18n:mobile.tags.mk-selectdrive-page.select-file%<span class="count" if={ files.length > 0 }>({ files.length })</span></h1>
+		<h1>%i18n:mobile.tags.mk-selectdrive-page.select-file%<span class="count" v-if="files.length > 0">({ files.length })</span></h1>
 		<button class="upload" @click="upload">%fa:upload%</button>
-		<button if={ multiple } class="ok" @click="ok">%fa:check%</button>
+		<button v-if="multiple" class="ok" @click="ok">%fa:check%</button>
 	</header>
 	<mk-drive ref="browser" select-file={ true } multiple={ multiple } is-naked={ true } top={ 42 }/>
 
diff --git a/src/web/app/mobile/tags/page/user-followers.tag b/src/web/app/mobile/tags/page/user-followers.tag
index 50280e7b9..626c8025d 100644
--- a/src/web/app/mobile/tags/page/user-followers.tag
+++ b/src/web/app/mobile/tags/page/user-followers.tag
@@ -1,6 +1,6 @@
 <mk-user-followers-page>
 	<mk-ui ref="ui">
-		<mk-user-followers ref="list" if={ !parent.fetching } user={ parent.user }/>
+		<mk-user-followers ref="list" v-if="!parent.fetching" user={ parent.user }/>
 	</mk-ui>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/mobile/tags/page/user-following.tag b/src/web/app/mobile/tags/page/user-following.tag
index b28efbab9..220c5fbf8 100644
--- a/src/web/app/mobile/tags/page/user-following.tag
+++ b/src/web/app/mobile/tags/page/user-following.tag
@@ -1,6 +1,6 @@
 <mk-user-following-page>
 	<mk-ui ref="ui">
-		<mk-user-following ref="list" if={ !parent.fetching } user={ parent.user }/>
+		<mk-user-following ref="list" v-if="!parent.fetching" user={ parent.user }/>
 	</mk-ui>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index e397ce7c0..1c936a8d7 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -1,17 +1,17 @@
 <mk-post-detail>
-	<button class="read-more" if={ p.reply && p.reply.reply_id && context == null } @click="loadContext" disabled={ loadingContext }>
-		<virtual if={ !contextFetching }>%fa:ellipsis-v%</virtual>
-		<virtual if={ contextFetching }>%fa:spinner .pulse%</virtual>
+	<button class="read-more" v-if="p.reply && p.reply.reply_id && context == null" @click="loadContext" disabled={ loadingContext }>
+		<virtual v-if="!contextFetching">%fa:ellipsis-v%</virtual>
+		<virtual v-if="contextFetching">%fa:spinner .pulse%</virtual>
 	</button>
 	<div class="context">
 		<virtual each={ post in context }>
 			<mk-post-detail-sub post={ post }/>
 		</virtual>
 	</div>
-	<div class="reply-to" if={ p.reply }>
+	<div class="reply-to" v-if="p.reply">
 		<mk-post-detail-sub post={ p.reply }/>
 	</div>
-	<div class="repost" if={ isRepost }>
+	<div class="repost" v-if="isRepost">
 		<p>
 			<a class="avatar-anchor" href={ '/' + post.user.username }>
 				<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=32' } alt="avatar"/></a>
@@ -33,10 +33,10 @@
 		</header>
 		<div class="body">
 			<div class="text" ref="text"></div>
-			<div class="media" if={ p.media }>
+			<div class="media" v-if="p.media">
 				<mk-images images={ p.media }/>
 			</div>
-			<mk-poll if={ p.poll } post={ p }/>
+			<mk-poll v-if="p.poll" post={ p }/>
 		</div>
 		<a class="time" href={ '/' + p.user.username + '/' + p.id }>
 			<mk-time time={ p.created_at } mode="detail"/>
@@ -44,20 +44,20 @@
 		<footer>
 			<mk-reactions-viewer post={ p }/>
 			<button @click="reply" title="%i18n:mobile.tags.mk-post-detail.reply%">
-				%fa:reply%<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
+				%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
 			</button>
 			<button @click="repost" title="Repost">
-				%fa:retweet%<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
+				%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
 			</button>
 			<button class={ reacted: p.my_reaction != null } @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-post-detail.reaction%">
-				%fa:plus%<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
+				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
 			</button>
 			<button @click="menu" ref="menuButton">
 				%fa:ellipsis-h%
 			</button>
 		</footer>
 	</article>
-	<div class="replies" if={ !compact }>
+	<div class="replies" v-if="!compact">
 		<virtual each={ post in replies }>
 			<mk-post-detail-sub post={ post }/>
 		</virtual>
diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
index 202b03c20..01c0748fe 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -2,12 +2,12 @@
 	<header>
 		<button class="cancel" @click="cancel">%fa:times%</button>
 		<div>
-			<span if={ refs.text } class="text-count { over: refs.text.value.length > 1000 }">{ 1000 - refs.text.value.length }</span>
+			<span v-if="refs.text" class="text-count { over: refs.text.value.length > 1000 }">{ 1000 - refs.text.value.length }</span>
 			<button class="submit" @click="post">%i18n:mobile.tags.mk-post-form.submit%</button>
 		</div>
 	</header>
 	<div class="form">
-		<mk-post-preview if={ opts.reply } post={ opts.reply }/>
+		<mk-post-preview v-if="opts.reply" post={ opts.reply }/>
 		<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ opts.reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.post-placeholder%' }></textarea>
 		<div class="attaches" show={ files.length != 0 }>
 			<ul class="files" ref="attaches">
@@ -16,7 +16,7 @@
 				</li>
 			</ul>
 		</div>
-		<mk-poll-editor if={ poll } ref="poll" ondestroy={ onPollDestroyed }/>
+		<mk-poll-editor v-if="poll" ref="poll" ondestroy={ onPollDestroyed }/>
 		<mk-uploader ref="uploader"/>
 		<button ref="upload" @click="selectFile">%fa:upload%</button>
 		<button ref="drive" @click="selectFileFromDrive">%fa:cloud%</button>
diff --git a/src/web/app/mobile/tags/sub-post-content.tag b/src/web/app/mobile/tags/sub-post-content.tag
index 3d9175b18..27f01fa07 100644
--- a/src/web/app/mobile/tags/sub-post-content.tag
+++ b/src/web/app/mobile/tags/sub-post-content.tag
@@ -1,10 +1,10 @@
 <mk-sub-post-content>
-	<div class="body"><a class="reply" if={ post.reply_id }>%fa:reply%</a><span ref="text"></span><a class="quote" if={ post.repost_id } href={ '/post:' + post.repost_id }>RP: ...</a></div>
-	<details if={ post.media }>
+	<div class="body"><a class="reply" v-if="post.reply_id">%fa:reply%</a><span ref="text"></span><a class="quote" v-if="post.repost_id" href={ '/post:' + post.repost_id }>RP: ...</a></div>
+	<details v-if="post.media">
 		<summary>({ post.media.length }個のメディア)</summary>
 		<mk-images images={ post.media }/>
 	</details>
-	<details if={ post.poll }>
+	<details v-if="post.poll">
 		<summary>%i18n:mobile.tags.mk-sub-post-content.poll%</summary>
 		<mk-poll post={ post }/>
 	</details>
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 3daf6b6d1..bf3fa0931 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -1,21 +1,21 @@
 <mk-timeline>
-	<div class="init" if={ init }>
+	<div class="init" v-if="init">
 		%fa:spinner .pulse%%i18n:common.loading%
 	</div>
-	<div class="empty" if={ !init && posts.length == 0 }>
+	<div class="empty" v-if="!init && posts.length == 0">
 		%fa:R comments%{ opts.empty || '%i18n:mobile.tags.mk-timeline.empty%' }
 	</div>
 	<virtual each={ post, i in posts }>
 		<mk-timeline-post post={ post }/>
-		<p class="date" if={ i != posts.length - 1 && post._date != posts[i + 1]._date }>
+		<p class="date" v-if="i != posts.length - 1 && post._date != posts[i + 1]._date">
 			<span>%fa:angle-up%{ post._datetext }</span>
 			<span>%fa:angle-down%{ posts[i + 1]._datetext }</span>
 		</p>
 	</virtual>
-	<footer if={ !init }>
-		<button if={ canFetchMore } @click="more" disabled={ fetching }>
-			<span if={ !fetching }>%i18n:mobile.tags.mk-timeline.load-more%</span>
-			<span if={ fetching }>%i18n:common.loading%<mk-ellipsis/></span>
+	<footer v-if="!init">
+		<button v-if="canFetchMore" @click="more" disabled={ fetching }>
+			<span v-if="!fetching">%i18n:mobile.tags.mk-timeline.load-more%</span>
+			<span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span>
 		</button>
 	</footer>
 	<style lang="stylus" scoped>
@@ -137,10 +137,10 @@
 </mk-timeline>
 
 <mk-timeline-post class={ repost: isRepost }>
-	<div class="reply-to" if={ p.reply }>
+	<div class="reply-to" v-if="p.reply">
 		<mk-timeline-post-sub post={ p.reply }/>
 	</div>
-	<div class="repost" if={ isRepost }>
+	<div class="repost" v-if="isRepost">
 		<p>
 			<a class="avatar-anchor" href={ '/' + post.user.username }>
 				<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
@@ -156,7 +156,7 @@
 		<div class="main">
 			<header>
 				<a class="name" href={ '/' + p.user.username }>{ p.user.name }</a>
-				<span class="is-bot" if={ p.user.is_bot }>bot</span>
+				<span class="is-bot" v-if="p.user.is_bot">bot</span>
 				<span class="username">@{ p.user.username }</span>
 				<a class="created-at" href={ url }>
 					<mk-time time={ p.created_at }/>
@@ -164,32 +164,32 @@
 			</header>
 			<div class="body">
 				<div class="text" ref="text">
-					<p class="channel" if={ p.channel != null }><a href={ _CH_URL_ + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
-					<a class="reply" if={ p.reply }>
+					<p class="channel" v-if="p.channel != null"><a href={ _CH_URL_ + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
+					<a class="reply" v-if="p.reply">
 						%fa:reply%
 					</a>
 					<p class="dummy"></p>
-					<a class="quote" if={ p.repost != null }>RP:</a>
+					<a class="quote" v-if="p.repost != null">RP:</a>
 				</div>
-				<div class="media" if={ p.media }>
+				<div class="media" v-if="p.media">
 					<mk-images images={ p.media }/>
 				</div>
-				<mk-poll if={ p.poll } post={ p } ref="pollViewer"/>
-				<span class="app" if={ p.app }>via <b>{ p.app.name }</b></span>
-				<div class="repost" if={ p.repost }>%fa:quote-right -flip-h%
+				<mk-poll v-if="p.poll" post={ p } ref="pollViewer"/>
+				<span class="app" v-if="p.app">via <b>{ p.app.name }</b></span>
+				<div class="repost" v-if="p.repost">%fa:quote-right -flip-h%
 					<mk-post-preview class="repost" post={ p.repost }/>
 				</div>
 			</div>
 			<footer>
 				<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
 				<button @click="reply">
-					%fa:reply%<p class="count" if={ p.replies_count > 0 }>{ p.replies_count }</p>
+					%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
 				</button>
 				<button @click="repost" title="Repost">
-					%fa:retweet%<p class="count" if={ p.repost_count > 0 }>{ p.repost_count }</p>
+					%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
 				</button>
 				<button class={ reacted: p.my_reaction != null } @click="react" ref="reactButton">
-					%fa:plus%<p class="count" if={ p.reactions_count > 0 }>{ p.reactions_count }</p>
+					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
 				</button>
 				<button class="menu" @click="menu" ref="menuButton">
 					%fa:ellipsis-h%
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index 8f0324f4d..0c783b8f3 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -4,7 +4,7 @@
 	<div class="content">
 		<yield />
 	</div>
-	<mk-stream-indicator if={ SIGNIN }/>
+	<mk-stream-indicator v-if="SIGNIN"/>
 	<style lang="stylus" scoped>
 		:scope
 			display block
@@ -53,9 +53,9 @@
 		<div class="backdrop"></div>
 		<div class="content">
 			<button class="nav" @click="parent.toggleDrawer">%fa:bars%</button>
-			<virtual if={ hasUnreadNotifications || hasUnreadMessagingMessages }>%fa:circle%</virtual>
+			<virtual v-if="hasUnreadNotifications || hasUnreadMessagingMessages">%fa:circle%</virtual>
 			<h1 ref="title">Misskey</h1>
-			<button if={ func } @click="func"><mk-raw content={ funcIcon }/></button>
+			<button v-if="func" @click="func"><mk-raw content={ funcIcon }/></button>
 		</div>
 	</div>
 	<style lang="stylus" scoped>
@@ -227,15 +227,15 @@
 <mk-ui-nav>
 	<div class="backdrop" @click="parent.toggleDrawer"></div>
 	<div class="body">
-		<a class="me" if={ SIGNIN } href={ '/' + I.username }>
+		<a class="me" v-if="SIGNIN" href={ '/' + I.username }>
 			<img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/>
 			<p class="name">{ I.name }</p>
 		</a>
 		<div class="links">
 			<ul>
 				<li><a href="/">%fa:home%%i18n:mobile.tags.mk-ui-nav.home%%fa:angle-right%</a></li>
-				<li><a href="/i/notifications">%fa:R bell%%i18n:mobile.tags.mk-ui-nav.notifications%<virtual if={ hasUnreadNotifications }>%fa:circle%</virtual>%fa:angle-right%</a></li>
-				<li><a href="/i/messaging">%fa:R comments%%i18n:mobile.tags.mk-ui-nav.messaging%<virtual if={ hasUnreadMessagingMessages }>%fa:circle%</virtual>%fa:angle-right%</a></li>
+				<li><a href="/i/notifications">%fa:R bell%%i18n:mobile.tags.mk-ui-nav.notifications%<virtual v-if="hasUnreadNotifications">%fa:circle%</virtual>%fa:angle-right%</a></li>
+				<li><a href="/i/messaging">%fa:R comments%%i18n:mobile.tags.mk-ui-nav.messaging%<virtual v-if="hasUnreadMessagingMessages">%fa:circle%</virtual>%fa:angle-right%</a></li>
 			</ul>
 			<ul>
 				<li><a href={ _CH_URL_ } target="_blank">%fa:tv%%i18n:mobile.tags.mk-ui-nav.ch%%fa:angle-right%</a></li>
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index f0ecbd1c3..316fb764e 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -1,5 +1,5 @@
 <mk-user>
-	<div class="user" if={ !fetching }>
+	<div class="user" v-if="!fetching">
 		<header>
 			<div class="banner" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=1024)' : '' }></div>
 			<div class="body">
@@ -7,19 +7,19 @@
 					<a class="avatar">
 						<img src={ user.avatar_url + '?thumbnail&size=200' } alt="avatar"/>
 					</a>
-					<mk-follow-button if={ SIGNIN && I.id != user.id } user={ user }/>
+					<mk-follow-button v-if="SIGNIN && I.id != user.id" user={ user }/>
 				</div>
 				<div class="title">
 					<h1>{ user.name }</h1>
 					<span class="username">@{ user.username }</span>
-					<span class="followed" if={ user.is_followed }>%i18n:mobile.tags.mk-user.follows-you%</span>
+					<span class="followed" v-if="user.is_followed">%i18n:mobile.tags.mk-user.follows-you%</span>
 				</div>
 				<div class="description">{ user.description }</div>
 				<div class="info">
-					<p class="location" if={ user.profile.location }>
+					<p class="location" v-if="user.profile.location">
 						%fa:map-marker%{ user.profile.location }
 					</p>
-					<p class="birthday" if={ user.profile.birthday }>
+					<p class="birthday" v-if="user.profile.birthday">
 						%fa:birthday-cake%{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' } ({ age(user.profile.birthday) }歳)
 					</p>
 				</div>
@@ -45,9 +45,9 @@
 			</nav>
 		</header>
 		<div class="body">
-			<mk-user-overview if={ page == 'overview' } user={ user }/>
-			<mk-user-timeline if={ page == 'posts' } user={ user }/>
-			<mk-user-timeline if={ page == 'media' } user={ user } with-media={ true }/>
+			<mk-user-overview v-if="page == 'overview'" user={ user }/>
+			<mk-user-timeline v-if="page == 'posts'" user={ user }/>
+			<mk-user-timeline v-if="page == 'media'" user={ user } with-media={ true }/>
 		</div>
 	</div>
 	<style lang="stylus" scoped>
@@ -215,7 +215,7 @@
 </mk-user>
 
 <mk-user-overview>
-	<mk-post-detail if={ user.pinned_post } post={ user.pinned_post } compact={ true }/>
+	<mk-post-detail v-if="user.pinned_post" post={ user.pinned_post } compact={ true }/>
 	<section class="recent-posts">
 		<h2>%fa:R comments%%i18n:mobile.tags.mk-user-overview.recent-posts%</h2>
 		<div>
@@ -252,7 +252,7 @@
 			<mk-user-overview-frequently-replied-users user={ user }/>
 		</div>
 	</section>
-	<section class="followers-you-know" if={ SIGNIN && I.id !== user.id }>
+	<section class="followers-you-know" v-if="SIGNIN && I.id !== user.id">
 		<h2>%fa:users%%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2>
 		<div>
 			<mk-user-overview-followers-you-know user={ user }/>
@@ -307,13 +307,13 @@
 </mk-user-overview>
 
 <mk-user-overview-posts>
-	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-posts.loading%<mk-ellipsis/></p>
-	<div if={ !initializing && posts.length > 0 }>
+	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-posts.loading%<mk-ellipsis/></p>
+	<div v-if="!initializing && posts.length > 0">
 		<virtual each={ posts }>
 			<mk-user-overview-posts-post-card post={ this }/>
 		</virtual>
 	</div>
-	<p class="empty" if={ !initializing && posts.length == 0 }>%i18n:mobile.tags.mk-user-overview-posts.no-posts%</p>
+	<p class="empty" v-if="!initializing && posts.length == 0">%i18n:mobile.tags.mk-user-overview-posts.no-posts%</p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
@@ -436,13 +436,13 @@
 </mk-user-overview-posts-post-card>
 
 <mk-user-overview-photos>
-	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-photos.loading%<mk-ellipsis/></p>
-	<div class="stream" if={ !initializing && images.length > 0 }>
+	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-photos.loading%<mk-ellipsis/></p>
+	<div class="stream" v-if="!initializing && images.length > 0">
 		<virtual each={ image in images }>
 			<a class="img" style={ 'background-image: url(' + image.media.url + '?thumbnail&size=256)' } href={ '/' + image.post.user.username + '/' + image.post.id }></a>
 		</virtual>
 	</div>
-	<p class="empty" if={ !initializing && images.length == 0 }>%i18n:mobile.tags.mk-user-overview-photos.no-photos%</p>
+	<p class="empty" v-if="!initializing && images.length == 0">%i18n:mobile.tags.mk-user-overview-photos.no-photos%</p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
@@ -506,7 +506,7 @@
 </mk-user-overview-photos>
 
 <mk-user-overview-activity-chart>
-	<svg if={ data } ref="canvas" viewBox="0 0 30 1" preserveAspectRatio="none">
+	<svg v-if="data" ref="canvas" viewBox="0 0 30 1" preserveAspectRatio="none">
 		<g each={ d, i in data.reverse() }>
 			<rect width="0.8" riot-height={ d.postsH }
 				riot-x={ i + 0.1 } riot-y={ 1 - d.postsH - d.repliesH - d.repostsH }
@@ -558,12 +558,12 @@
 </mk-user-overview-activity-chart>
 
 <mk-user-overview-keywords>
-	<div if={ user.keywords != null && user.keywords.length > 1 }>
+	<div v-if="user.keywords != null && user.keywords.length > 1">
 		<virtual each={ keyword in user.keywords }>
 			<a>{ keyword }</a>
 		</virtual>
 	</div>
-	<p class="empty" if={ user.keywords == null || user.keywords.length == 0 }>%i18n:mobile.tags.mk-user-overview-keywords.no-keywords%</p>
+	<p class="empty" v-if="user.keywords == null || user.keywords.length == 0">%i18n:mobile.tags.mk-user-overview-keywords.no-keywords%</p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
@@ -592,12 +592,12 @@
 </mk-user-overview-keywords>
 
 <mk-user-overview-domains>
-	<div if={ user.domains != null && user.domains.length > 1 }>
+	<div v-if="user.domains != null && user.domains.length > 1">
 		<virtual each={ domain in user.domains }>
 			<a style="opacity: { 0.5 + (domain.weight / 2) }">{ domain.domain }</a>
 		</virtual>
 	</div>
-	<p class="empty" if={ user.domains == null || user.domains.length == 0 }>%i18n:mobile.tags.mk-user-overview-domains.no-domains%</p>
+	<p class="empty" v-if="user.domains == null || user.domains.length == 0">%i18n:mobile.tags.mk-user-overview-domains.no-domains%</p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
@@ -626,13 +626,13 @@
 </mk-user-overview-domains>
 
 <mk-user-overview-frequently-replied-users>
-	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-frequently-replied-users.loading%<mk-ellipsis/></p>
-	<div if={ !initializing && users.length > 0 }>
+	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-frequently-replied-users.loading%<mk-ellipsis/></p>
+	<div v-if="!initializing && users.length > 0">
 		<virtual each={ users }>
 			<mk-user-card user={ this.user }/>
 		</virtual>
 	</div>
-	<p class="empty" if={ !initializing && users.length == 0 }>%i18n:mobile.tags.mk-user-overview-frequently-replied-users.no-users%</p>
+	<p class="empty" v-if="!initializing && users.length == 0">%i18n:mobile.tags.mk-user-overview-frequently-replied-users.no-users%</p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
@@ -678,13 +678,13 @@
 </mk-user-overview-frequently-replied-users>
 
 <mk-user-overview-followers-you-know>
-	<p class="initializing" if={ initializing }>%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p>
-	<div if={ !initializing && users.length > 0 }>
+	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p>
+	<div v-if="!initializing && users.length > 0">
 		<virtual each={ user in users }>
 			<a href={ '/' + user.username }><img src={ user.avatar_url + '?thumbnail&size=64' } alt={ user.name }/></a>
 		</virtual>
 	</div>
-	<p class="empty" if={ !initializing && users.length == 0 }>%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p>
+	<p class="empty" v-if="!initializing && users.length == 0">%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/mobile/tags/users-list.tag b/src/web/app/mobile/tags/users-list.tag
index 31ca58185..17b69e9e1 100644
--- a/src/web/app/mobile/tags/users-list.tag
+++ b/src/web/app/mobile/tags/users-list.tag
@@ -1,16 +1,16 @@
 <mk-users-list>
 	<nav>
 		<span data-is-active={ mode == 'all' } @click="setMode.bind(this, 'all')">%i18n:mobile.tags.mk-users-list.all%<span>{ opts.count }</span></span>
-		<span if={ SIGNIN && opts.youKnowCount } data-is-active={ mode == 'iknow' } @click="setMode.bind(this, 'iknow')">%i18n:mobile.tags.mk-users-list.known%<span>{ opts.youKnowCount }</span></span>
+		<span v-if="SIGNIN && opts.youKnowCount" data-is-active={ mode == 'iknow' } @click="setMode.bind(this, 'iknow')">%i18n:mobile.tags.mk-users-list.known%<span>{ opts.youKnowCount }</span></span>
 	</nav>
-	<div class="users" if={ !fetching && users.length != 0 }>
+	<div class="users" v-if="!fetching && users.length != 0">
 		<mk-user-preview each={ users } user={ this }/>
 	</div>
-	<button class="more" if={ !fetching && next != null } @click="more" disabled={ moreFetching }>
-		<span if={ !moreFetching }>%i18n:mobile.tags.mk-users-list.load-more%</span>
-		<span if={ moreFetching }>%i18n:common.loading%<mk-ellipsis/></span></button>
-	<p class="no" if={ !fetching && users.length == 0 }>{ opts.noUsers }</p>
-	<p class="fetching" if={ fetching }>%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<button class="more" v-if="!fetching && next != null" @click="more" disabled={ moreFetching }>
+		<span v-if="!moreFetching">%i18n:mobile.tags.mk-users-list.load-more%</span>
+		<span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span></button>
+	<p class="no" v-if="!fetching && users.length == 0">{ opts.noUsers }</p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/stats/tags/index.tag b/src/web/app/stats/tags/index.tag
index 494983706..84866c3d1 100644
--- a/src/web/app/stats/tags/index.tag
+++ b/src/web/app/stats/tags/index.tag
@@ -1,6 +1,6 @@
 <mk-index>
 	<h1>Misskey<i>Statistics</i></h1>
-	<main if={ !initializing }>
+	<main v-if="!initializing">
 		<mk-users stats={ stats }/>
 		<mk-posts stats={ stats }/>
 	</main>
@@ -58,7 +58,7 @@
 
 <mk-posts>
 	<h2>%i18n:stats.posts-count% <b>{ stats.posts_count }</b></h2>
-	<mk-posts-chart if={ !initializing } data={ data }/>
+	<mk-posts-chart v-if="!initializing" data={ data }/>
 	<style lang="stylus" scoped>
 		:scope
 			display block
@@ -84,7 +84,7 @@
 
 <mk-users>
 	<h2>%i18n:stats.users-count% <b>{ stats.users_count }</b></h2>
-	<mk-users-chart if={ !initializing } data={ data }/>
+	<mk-users-chart v-if="!initializing" data={ data }/>
 	<style lang="stylus" scoped>
 		:scope
 			display block

From 853b846c4017d0edd7d4d0f23812f674d598d6d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 7 Feb 2018 18:41:48 +0900
Subject: [PATCH 009/286] wip

---
 src/web/app/common/tags/reactions-viewer.vue | 80 +++++++++-----------
 1 file changed, 37 insertions(+), 43 deletions(-)

diff --git a/src/web/app/common/tags/reactions-viewer.vue b/src/web/app/common/tags/reactions-viewer.vue
index ad126ff1d..18002c972 100644
--- a/src/web/app/common/tags/reactions-viewer.vue
+++ b/src/web/app/common/tags/reactions-viewer.vue
@@ -1,55 +1,49 @@
 <template>
 <div>
 	<template v-if="reactions">
-		<span v-if="reactions.like"><mk-reaction-icon reaction='like'/><span>{ reactions.like }</span></span>
-		<span v-if="reactions.love"><mk-reaction-icon reaction='love'/><span>{ reactions.love }</span></span>
-		<span v-if="reactions.laugh"><mk-reaction-icon reaction='laugh'/><span>{ reactions.laugh }</span></span>
-		<span v-if="reactions.hmm"><mk-reaction-icon reaction='hmm'/><span>{ reactions.hmm }</span></span>
-		<span v-if="reactions.surprise"><mk-reaction-icon reaction='surprise'/><span>{ reactions.surprise }</span></span>
-		<span v-if="reactions.congrats"><mk-reaction-icon reaction='congrats'/><span>{ reactions.congrats }</span></span>
-		<span v-if="reactions.angry"><mk-reaction-icon reaction='angry'/><span>{ reactions.angry }</span></span>
-		<span v-if="reactions.confused"><mk-reaction-icon reaction='confused'/><span>{ reactions.confused }</span></span>
-		<span v-if="reactions.pudding"><mk-reaction-icon reaction='pudding'/><span>{ reactions.pudding }</span></span>
+		<span v-if="reactions.like"><mk-reaction-icon reaction='like'/><span>{{ reactions.like }}</span></span>
+		<span v-if="reactions.love"><mk-reaction-icon reaction='love'/><span>{{ reactions.love }}</span></span>
+		<span v-if="reactions.laugh"><mk-reaction-icon reaction='laugh'/><span>{{ reactions.laugh }}</span></span>
+		<span v-if="reactions.hmm"><mk-reaction-icon reaction='hmm'/><span>{{ reactions.hmm }}</span></span>
+		<span v-if="reactions.surprise"><mk-reaction-icon reaction='surprise'/><span>{{ reactions.surprise }}</span></span>
+		<span v-if="reactions.congrats"><mk-reaction-icon reaction='congrats'/><span>{{ reactions.congrats }}</span></span>
+		<span v-if="reactions.angry"><mk-reaction-icon reaction='angry'/><span>{{ reactions.angry }}</span></span>
+		<span v-if="reactions.confused"><mk-reaction-icon reaction='confused'/><span>{{ reactions.confused }}</span></span>
+		<span v-if="reactions.pudding"><mk-reaction-icon reaction='pudding'/><span>{{ reactions.pudding }}</span></span>
 	</template>
 </div>
 </template>
 
+<script>
+	export default {
+		props: ['post'],
+		computed: {
+			reactions: function() {
+				return this.post.reaction_counts;
+			}
+		}
+	};
+</script>
 
-<mk-reactions-viewer>
-	<virtual v-if="reactions">
-		
-	</virtual>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			border-top dashed 1px #eee
-			border-bottom dashed 1px #eee
-			margin 4px 0
+<style lang="stylus" scoped>
+	:scope
+		display block
+		border-top dashed 1px #eee
+		border-bottom dashed 1px #eee
+		margin 4px 0
 
-			&:empty
-				display none
+		&:empty
+			display none
+
+		> span
+			margin-right 8px
+
+			> mk-reaction-icon
+				font-size 1.4em
 
 			> span
-				margin-right 8px
+				margin-left 4px
+				font-size 1.2em
+				color #444
 
-				> mk-reaction-icon
-					font-size 1.4em
-
-				> span
-					margin-left 4px
-					font-size 1.2em
-					color #444
-
-	</style>
-	<script>
-		this.post = this.opts.post;
-
-		this.on('mount', () => {
-			this.update();
-		});
-
-		this.on('update', () => {
-			this.reactions = this.post.reaction_counts;
-		});
-	</script>
-</mk-reactions-viewer>
+</style>

From d8d4c4d2287489a02b3185a79ed0cac77057cf81 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 7 Feb 2018 18:47:29 +0900
Subject: [PATCH 010/286] wip

---
 src/web/app/auth/tags/form.tag                |  2 +-
 src/web/app/auth/tags/index.tag               |  2 +-
 src/web/app/ch/tags/channel.tag               | 10 +++++-----
 src/web/app/ch/tags/header.tag                |  2 +-
 src/web/app/ch/tags/index.tag                 |  2 +-
 src/web/app/common/tags/activity-table.tag    |  2 +-
 src/web/app/common/tags/authorized-apps.tag   |  2 +-
 src/web/app/common/tags/error.tag             |  4 ++--
 src/web/app/common/tags/file-type-icon.tag    |  2 +-
 src/web/app/common/tags/messaging/form.tag    |  2 +-
 src/web/app/common/tags/messaging/index.tag   |  2 +-
 src/web/app/common/tags/messaging/message.tag |  2 +-
 src/web/app/common/tags/messaging/room.tag    |  2 +-
 src/web/app/common/tags/nav-links.tag         |  2 +-
 src/web/app/common/tags/number.tag            |  2 +-
 src/web/app/common/tags/poll-editor.tag       |  2 +-
 src/web/app/common/tags/poll.tag              |  2 +-
 src/web/app/common/tags/post-menu.tag         |  2 +-
 src/web/app/common/tags/raw.tag               |  2 +-
 src/web/app/common/tags/reaction-picker.vue   |  2 +-
 src/web/app/common/tags/reactions-viewer.vue  |  2 +-
 src/web/app/common/tags/signin-history.tag    |  4 ++--
 src/web/app/common/tags/signin.tag            |  2 +-
 src/web/app/common/tags/signup.tag            |  2 +-
 src/web/app/common/tags/special-message.tag   |  2 +-
 src/web/app/common/tags/stream-indicator.vue  |  2 +-
 src/web/app/common/tags/time.vue              |  2 +-
 src/web/app/common/tags/twitter-setting.tag   |  2 +-
 src/web/app/common/tags/uploader.tag          |  2 +-
 src/web/app/desktop/tags/analog-clock.tag     |  2 +-
 .../desktop/tags/autocomplete-suggestion.tag  |  2 +-
 .../app/desktop/tags/big-follow-button.tag    |  2 +-
 src/web/app/desktop/tags/contextmenu.tag      |  2 +-
 src/web/app/desktop/tags/crop-window.tag      |  2 +-
 .../app/desktop/tags/detailed-post-window.tag |  2 +-
 src/web/app/desktop/tags/dialog.tag           |  2 +-
 src/web/app/desktop/tags/donation.tag         |  2 +-
 .../desktop/tags/drive/base-contextmenu.tag   |  2 +-
 .../app/desktop/tags/drive/browser-window.tag |  2 +-
 src/web/app/desktop/tags/drive/browser.tag    |  2 +-
 .../desktop/tags/drive/file-contextmenu.tag   |  2 +-
 src/web/app/desktop/tags/drive/file.tag       |  2 +-
 .../desktop/tags/drive/folder-contextmenu.tag |  2 +-
 src/web/app/desktop/tags/drive/folder.tag     |  2 +-
 src/web/app/desktop/tags/drive/nav-folder.tag |  2 +-
 src/web/app/desktop/tags/follow-button.tag    |  2 +-
 .../app/desktop/tags/following-setuper.tag    |  2 +-
 .../desktop/tags/home-widgets/access-log.tag  |  2 +-
 .../desktop/tags/home-widgets/activity.tag    |  2 +-
 .../desktop/tags/home-widgets/broadcast.tag   |  2 +-
 .../desktop/tags/home-widgets/calendar.tag    |  2 +-
 .../app/desktop/tags/home-widgets/channel.tag |  8 ++++----
 .../desktop/tags/home-widgets/donation.tag    |  2 +-
 .../desktop/tags/home-widgets/mentions.tag    |  2 +-
 .../desktop/tags/home-widgets/messaging.tag   |  2 +-
 src/web/app/desktop/tags/home-widgets/nav.tag |  2 +-
 .../tags/home-widgets/notifications.tag       |  2 +-
 .../tags/home-widgets/photo-stream.tag        |  2 +-
 .../desktop/tags/home-widgets/post-form.tag   |  2 +-
 .../app/desktop/tags/home-widgets/profile.tag |  2 +-
 .../tags/home-widgets/recommended-polls.tag   |  2 +-
 .../desktop/tags/home-widgets/rss-reader.tag  |  2 +-
 .../app/desktop/tags/home-widgets/server.tag  | 16 +++++++--------
 .../desktop/tags/home-widgets/slideshow.tag   |  2 +-
 .../desktop/tags/home-widgets/timeline.tag    |  2 +-
 .../desktop/tags/home-widgets/timemachine.tag |  2 +-
 .../app/desktop/tags/home-widgets/tips.tag    |  2 +-
 .../app/desktop/tags/home-widgets/trends.tag  |  2 +-
 .../tags/home-widgets/user-recommendation.tag |  2 +-
 .../app/desktop/tags/home-widgets/version.tag |  2 +-
 src/web/app/desktop/tags/home.tag             |  2 +-
 src/web/app/desktop/tags/images.tag           |  6 +++---
 src/web/app/desktop/tags/input-dialog.tag     |  2 +-
 src/web/app/desktop/tags/list-user.tag        |  2 +-
 .../desktop/tags/messaging/room-window.tag    |  2 +-
 src/web/app/desktop/tags/messaging/window.tag |  2 +-
 src/web/app/desktop/tags/notifications.tag    |  2 +-
 src/web/app/desktop/tags/pages/drive.tag      |  2 +-
 src/web/app/desktop/tags/pages/entrance.tag   |  4 ++--
 .../app/desktop/tags/pages/home-customize.tag |  2 +-
 src/web/app/desktop/tags/pages/home.tag       |  2 +-
 .../app/desktop/tags/pages/messaging-room.tag |  2 +-
 src/web/app/desktop/tags/pages/post.tag       |  2 +-
 src/web/app/desktop/tags/pages/search.tag     |  2 +-
 .../app/desktop/tags/pages/selectdrive.tag    |  2 +-
 src/web/app/desktop/tags/pages/user.tag       |  2 +-
 src/web/app/desktop/tags/post-detail-sub.tag  |  2 +-
 src/web/app/desktop/tags/post-detail.tag      |  2 +-
 src/web/app/desktop/tags/post-form-window.tag |  2 +-
 src/web/app/desktop/tags/post-form.tag        |  2 +-
 src/web/app/desktop/tags/post-preview.tag     |  2 +-
 src/web/app/desktop/tags/progress-dialog.tag  |  2 +-
 .../app/desktop/tags/repost-form-window.tag   |  2 +-
 src/web/app/desktop/tags/repost-form.tag      |  2 +-
 src/web/app/desktop/tags/search-posts.tag     |  2 +-
 src/web/app/desktop/tags/search.tag           |  2 +-
 .../tags/select-file-from-drive-window.tag    |  2 +-
 .../tags/select-folder-from-drive-window.tag  |  2 +-
 .../desktop/tags/set-avatar-suggestion.tag    |  2 +-
 .../desktop/tags/set-banner-suggestion.tag    |  2 +-
 src/web/app/desktop/tags/settings-window.tag  |  2 +-
 src/web/app/desktop/tags/settings.tag         | 14 ++++++-------
 src/web/app/desktop/tags/sub-post-content.tag |  2 +-
 src/web/app/desktop/tags/timeline.tag         |  6 +++---
 src/web/app/desktop/tags/ui.tag               | 18 ++++++++---------
 .../desktop/tags/user-followers-window.tag    |  2 +-
 src/web/app/desktop/tags/user-followers.tag   |  2 +-
 .../desktop/tags/user-following-window.tag    |  2 +-
 src/web/app/desktop/tags/user-following.tag   |  2 +-
 src/web/app/desktop/tags/user-preview.tag     |  2 +-
 src/web/app/desktop/tags/user-timeline.tag    |  2 +-
 src/web/app/desktop/tags/user.tag             | 18 ++++++++---------
 src/web/app/desktop/tags/users-list.tag       |  2 +-
 src/web/app/desktop/tags/widgets/activity.tag |  6 +++---
 src/web/app/desktop/tags/widgets/calendar.tag |  2 +-
 src/web/app/desktop/tags/window.tag           |  2 +-
 src/web/app/dev/tags/new-app-form.tag         |  2 +-
 src/web/app/dev/tags/pages/app.tag            |  2 +-
 src/web/app/dev/tags/pages/apps.tag           |  2 +-
 .../app/mobile/tags/drive-folder-selector.tag |  2 +-
 src/web/app/mobile/tags/drive-selector.tag    |  2 +-
 src/web/app/mobile/tags/drive.tag             |  2 +-
 src/web/app/mobile/tags/drive/file-viewer.tag |  2 +-
 src/web/app/mobile/tags/drive/file.tag        |  2 +-
 src/web/app/mobile/tags/drive/folder.tag      |  2 +-
 src/web/app/mobile/tags/follow-button.tag     |  2 +-
 src/web/app/mobile/tags/home-timeline.tag     |  2 +-
 src/web/app/mobile/tags/home.tag              |  2 +-
 src/web/app/mobile/tags/images.tag            |  4 ++--
 src/web/app/mobile/tags/init-following.tag    |  2 +-
 .../app/mobile/tags/notification-preview.tag  |  2 +-
 src/web/app/mobile/tags/notification.tag      |  2 +-
 src/web/app/mobile/tags/notifications.tag     |  2 +-
 src/web/app/mobile/tags/notify.tag            |  2 +-
 src/web/app/mobile/tags/page/drive.tag        |  2 +-
 src/web/app/mobile/tags/page/entrance.tag     |  2 +-
 src/web/app/mobile/tags/page/home.tag         |  2 +-
 .../app/mobile/tags/page/messaging-room.tag   |  2 +-
 src/web/app/mobile/tags/page/messaging.tag    |  2 +-
 .../app/mobile/tags/page/notifications.tag    |  2 +-
 src/web/app/mobile/tags/page/post.tag         |  2 +-
 src/web/app/mobile/tags/page/search.tag       |  2 +-
 src/web/app/mobile/tags/page/selectdrive.tag  |  2 +-
 src/web/app/mobile/tags/page/settings.tag     |  4 ++--
 .../tags/page/settings/authorized-apps.tag    |  2 +-
 .../app/mobile/tags/page/settings/profile.tag |  4 ++--
 .../app/mobile/tags/page/settings/signin.tag  |  2 +-
 .../app/mobile/tags/page/settings/twitter.tag |  2 +-
 .../app/mobile/tags/page/user-followers.tag   |  2 +-
 .../app/mobile/tags/page/user-following.tag   |  2 +-
 src/web/app/mobile/tags/page/user.tag         |  2 +-
 src/web/app/mobile/tags/post-detail.tag       |  4 ++--
 src/web/app/mobile/tags/post-form.tag         |  2 +-
 src/web/app/mobile/tags/post-preview.tag      |  2 +-
 src/web/app/mobile/tags/search-posts.tag      |  2 +-
 src/web/app/mobile/tags/search.tag            |  2 +-
 src/web/app/mobile/tags/sub-post-content.tag  |  2 +-
 src/web/app/mobile/tags/timeline.tag          |  6 +++---
 src/web/app/mobile/tags/ui.tag                |  6 +++---
 src/web/app/mobile/tags/user-card.tag         |  2 +-
 src/web/app/mobile/tags/user-followers.tag    |  2 +-
 src/web/app/mobile/tags/user-following.tag    |  2 +-
 src/web/app/mobile/tags/user-preview.tag      |  2 +-
 src/web/app/mobile/tags/user-timeline.tag     |  2 +-
 src/web/app/mobile/tags/user.tag              | 20 +++++++++----------
 src/web/app/mobile/tags/users-list.tag        |  2 +-
 src/web/app/stats/tags/index.tag              | 10 +++++-----
 src/web/app/status/tags/index.tag             |  8 ++++----
 168 files changed, 237 insertions(+), 237 deletions(-)

diff --git a/src/web/app/auth/tags/form.tag b/src/web/app/auth/tags/form.tag
index 8c085ee9b..9b317fef4 100644
--- a/src/web/app/auth/tags/form.tag
+++ b/src/web/app/auth/tags/form.tag
@@ -105,7 +105,7 @@
 						font-size 16px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.session = this.opts.session;
diff --git a/src/web/app/auth/tags/index.tag b/src/web/app/auth/tags/index.tag
index 195c66909..e6b1cdb3f 100644
--- a/src/web/app/auth/tags/index.tag
+++ b/src/web/app/auth/tags/index.tag
@@ -83,7 +83,7 @@
 					margin 0 auto
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 		this.mixin('api');
 
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index b01c2b548..a706a247f 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -53,7 +53,7 @@
 					max-width 500px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import Progress from '../../common/scripts/loading';
 		import ChannelStream from '../../common/scripts/streaming/channel-stream';
 
@@ -228,7 +228,7 @@
 							vertical-align bottom
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.post = this.opts.post;
 		this.form = this.opts.form;
 
@@ -282,7 +282,7 @@
 				display none
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.channel = this.opts.channel;
@@ -375,7 +375,7 @@
 
 <mk-twitter-button>
 	<a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-show-count="false">Tweet</a>
-	<script>
+	<script lang="typescript">
 		this.on('mount', () => {
 			const head = document.getElementsByTagName('head')[0];
 			const script = document.createElement('script');
@@ -388,7 +388,7 @@
 
 <mk-line-button>
 	<div class="line-it-button" data-lang="ja" data-type="share-a" data-url={ _CH_URL_ } style="display: none;"></div>
-	<script>
+	<script lang="typescript">
 		this.on('mount', () => {
 			const head = document.getElementsByTagName('head')[0];
 			const script = document.createElement('script');
diff --git a/src/web/app/ch/tags/header.tag b/src/web/app/ch/tags/header.tag
index 84575b03d..47a1e3e76 100644
--- a/src/web/app/ch/tags/header.tag
+++ b/src/web/app/ch/tags/header.tag
@@ -14,7 +14,7 @@
 				margin-left auto
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 	</script>
 </mk-header>
diff --git a/src/web/app/ch/tags/index.tag b/src/web/app/ch/tags/index.tag
index e058da6a3..6e0b451e8 100644
--- a/src/web/app/ch/tags/index.tag
+++ b/src/web/app/ch/tags/index.tag
@@ -11,7 +11,7 @@
 			display block
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.on('mount', () => {
diff --git a/src/web/app/common/tags/activity-table.tag b/src/web/app/common/tags/activity-table.tag
index 39d4d7205..2f716912f 100644
--- a/src/web/app/common/tags/activity-table.tag
+++ b/src/web/app/common/tags/activity-table.tag
@@ -25,7 +25,7 @@
 					transform-origin center
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.user = this.opts.user;
diff --git a/src/web/app/common/tags/authorized-apps.tag b/src/web/app/common/tags/authorized-apps.tag
index 0511c1bc6..26efa1316 100644
--- a/src/web/app/common/tags/authorized-apps.tag
+++ b/src/web/app/common/tags/authorized-apps.tag
@@ -18,7 +18,7 @@
 					border-bottom solid 1px #eee
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.apps = [];
diff --git a/src/web/app/common/tags/error.tag b/src/web/app/common/tags/error.tag
index f72f403a9..6cf13666d 100644
--- a/src/web/app/common/tags/error.tag
+++ b/src/web/app/common/tags/error.tag
@@ -75,7 +75,7 @@
 					height 150px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.troubleshooting = false;
 
 		this.on('mount', () => {
@@ -169,7 +169,7 @@
 						color #ad4339
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.on('mount', () => {
 			this.update({
 				network: navigator.onLine
diff --git a/src/web/app/common/tags/file-type-icon.tag b/src/web/app/common/tags/file-type-icon.tag
index d47f96fd0..a3e479273 100644
--- a/src/web/app/common/tags/file-type-icon.tag
+++ b/src/web/app/common/tags/file-type-icon.tag
@@ -4,7 +4,7 @@
 		:scope
 			display inline
 	</style>
-	<script>
+	<script lang="typescript">
 		this.kind = this.opts.type.split('/')[0];
 	</script>
 </mk-file-type-icon>
diff --git a/src/web/app/common/tags/messaging/form.tag b/src/web/app/common/tags/messaging/form.tag
index df0658741..e9d2c01ca 100644
--- a/src/web/app/common/tags/messaging/form.tag
+++ b/src/web/app/common/tags/messaging/form.tag
@@ -116,7 +116,7 @@
 				display none
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.onpaste = e => {
diff --git a/src/web/app/common/tags/messaging/index.tag b/src/web/app/common/tags/messaging/index.tag
index fa12a78d8..6c25452c0 100644
--- a/src/web/app/common/tags/messaging/index.tag
+++ b/src/web/app/common/tags/messaging/index.tag
@@ -329,7 +329,7 @@
 								margin 0 12px 0 0
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 		this.mixin('api');
 
diff --git a/src/web/app/common/tags/messaging/message.tag b/src/web/app/common/tags/messaging/message.tag
index 4f75e9049..2f193aa5d 100644
--- a/src/web/app/common/tags/messaging/message.tag
+++ b/src/web/app/common/tags/messaging/message.tag
@@ -205,7 +205,7 @@
 						opacity 0.5
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import compile from '../../../common/scripts/text-compiler';
 
 		this.mixin('i');
diff --git a/src/web/app/common/tags/messaging/room.tag b/src/web/app/common/tags/messaging/room.tag
index e659b778b..91b93c482 100644
--- a/src/web/app/common/tags/messaging/room.tag
+++ b/src/web/app/common/tags/messaging/room.tag
@@ -161,7 +161,7 @@
 						//background rgba(0, 0, 0, 0.2)
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import MessagingStreamConnection from '../../scripts/streaming/messaging-stream';
 
 		this.mixin('i');
diff --git a/src/web/app/common/tags/nav-links.tag b/src/web/app/common/tags/nav-links.tag
index 3766e5c0a..3f2613c16 100644
--- a/src/web/app/common/tags/nav-links.tag
+++ b/src/web/app/common/tags/nav-links.tag
@@ -4,7 +4,7 @@
 		:scope
 			display inline
 	</style>
-	<script>
+	<script lang="typescript">
 		this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/about`;
 	</script>
 </mk-nav-links>
diff --git a/src/web/app/common/tags/number.tag b/src/web/app/common/tags/number.tag
index 4b1081a87..9cbbacd2c 100644
--- a/src/web/app/common/tags/number.tag
+++ b/src/web/app/common/tags/number.tag
@@ -3,7 +3,7 @@
 		:scope
 			display inline
 	</style>
-	<script>
+	<script lang="typescript">
 		this.on('mount', () => {
 			let value = this.opts.value;
 			const max = this.opts.max;
diff --git a/src/web/app/common/tags/poll-editor.tag b/src/web/app/common/tags/poll-editor.tag
index 1d57eb9de..0de26f654 100644
--- a/src/web/app/common/tags/poll-editor.tag
+++ b/src/web/app/common/tags/poll-editor.tag
@@ -85,7 +85,7 @@
 					color darken($theme-color, 30%)
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.choices = ['', ''];
 
 		this.oninput = (i, e) => {
diff --git a/src/web/app/common/tags/poll.tag b/src/web/app/common/tags/poll.tag
index e6971d5bb..c0605d890 100644
--- a/src/web/app/common/tags/poll.tag
+++ b/src/web/app/common/tags/poll.tag
@@ -67,7 +67,7 @@
 						background transparent
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.init = post => {
diff --git a/src/web/app/common/tags/post-menu.tag b/src/web/app/common/tags/post-menu.tag
index f3b13c0b1..c2b362e8b 100644
--- a/src/web/app/common/tags/post-menu.tag
+++ b/src/web/app/common/tags/post-menu.tag
@@ -74,7 +74,7 @@
 					display block
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import anime from 'animejs';
 
 		this.mixin('i');
diff --git a/src/web/app/common/tags/raw.tag b/src/web/app/common/tags/raw.tag
index 55de0962e..149ac6c4b 100644
--- a/src/web/app/common/tags/raw.tag
+++ b/src/web/app/common/tags/raw.tag
@@ -3,7 +3,7 @@
 		:scope
 			display inline
 	</style>
-	<script>
+	<script lang="typescript">
 		this.root.innerHTML = this.opts.content;
 
 		this.on('updated', () => {
diff --git a/src/web/app/common/tags/reaction-picker.vue b/src/web/app/common/tags/reaction-picker.vue
index 307b158c6..8f0f8956e 100644
--- a/src/web/app/common/tags/reaction-picker.vue
+++ b/src/web/app/common/tags/reaction-picker.vue
@@ -18,7 +18,7 @@
 </div>
 </template>
 
-<script>
+<script lang="typescript">
 	import anime from 'animejs';
 	import api from '../scripts/api';
 
diff --git a/src/web/app/common/tags/reactions-viewer.vue b/src/web/app/common/tags/reactions-viewer.vue
index 18002c972..32fa50801 100644
--- a/src/web/app/common/tags/reactions-viewer.vue
+++ b/src/web/app/common/tags/reactions-viewer.vue
@@ -14,7 +14,7 @@
 </div>
 </template>
 
-<script>
+<script lang="typescript">
 	export default {
 		props: ['post'],
 		computed: {
diff --git a/src/web/app/common/tags/signin-history.tag b/src/web/app/common/tags/signin-history.tag
index e6b57c091..cc9d2113f 100644
--- a/src/web/app/common/tags/signin-history.tag
+++ b/src/web/app/common/tags/signin-history.tag
@@ -7,7 +7,7 @@
 			display block
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 		this.mixin('api');
 
@@ -97,7 +97,7 @@
 
 	</style>
 
-	<script>
+	<script lang="typescript">
 		import hljs from 'highlight.js';
 
 		this.rec = this.opts.rec;
diff --git a/src/web/app/common/tags/signin.tag b/src/web/app/common/tags/signin.tag
index 3fa253fbb..441a8ec56 100644
--- a/src/web/app/common/tags/signin.tag
+++ b/src/web/app/common/tags/signin.tag
@@ -100,7 +100,7 @@
 						opacity 0.7
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.user = null;
diff --git a/src/web/app/common/tags/signup.tag b/src/web/app/common/tags/signup.tag
index 1efb4aa09..4e79de787 100644
--- a/src/web/app/common/tags/signup.tag
+++ b/src/web/app/common/tags/signup.tag
@@ -173,7 +173,7 @@
 						background darken($theme-color, 5%)
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 		const getPasswordStrength = require('syuilo-password-strength');
 
diff --git a/src/web/app/common/tags/special-message.tag b/src/web/app/common/tags/special-message.tag
index 24fe66652..da903c632 100644
--- a/src/web/app/common/tags/special-message.tag
+++ b/src/web/app/common/tags/special-message.tag
@@ -19,7 +19,7 @@
 				background #ff1036
 
 	</style>
-	<script>
+	<script lang="typescript">
 		const now = new Date();
 		this.d = now.getDate();
 		this.m = now.getMonth() + 1;
diff --git a/src/web/app/common/tags/stream-indicator.vue b/src/web/app/common/tags/stream-indicator.vue
index 6964cda34..ea8fa5adf 100644
--- a/src/web/app/common/tags/stream-indicator.vue
+++ b/src/web/app/common/tags/stream-indicator.vue
@@ -15,7 +15,7 @@
 	</div>
 </template>
 
-<script>
+<script lang="typescript">
 	import anime from 'animejs';
 	import Ellipsis from './ellipsis.vue';
 
diff --git a/src/web/app/common/tags/time.vue b/src/web/app/common/tags/time.vue
index 14f38eb2d..82d8ecbfd 100644
--- a/src/web/app/common/tags/time.vue
+++ b/src/web/app/common/tags/time.vue
@@ -6,7 +6,7 @@
 	</time>
 </template>
 
-<script>
+<script lang="typescript">
 	export default {
 		props: ['time', 'mode'],
 		data: {
diff --git a/src/web/app/common/tags/twitter-setting.tag b/src/web/app/common/tags/twitter-setting.tag
index cb3d1e56a..935239f44 100644
--- a/src/web/app/common/tags/twitter-setting.tag
+++ b/src/web/app/common/tags/twitter-setting.tag
@@ -24,7 +24,7 @@
 			.id
 				color #8899a6
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 
 		this.form = null;
diff --git a/src/web/app/common/tags/uploader.tag b/src/web/app/common/tags/uploader.tag
index cc555304d..1dbfff96f 100644
--- a/src/web/app/common/tags/uploader.tag
+++ b/src/web/app/common/tags/uploader.tag
@@ -138,7 +138,7 @@
 							to   {background-position: -64px 32px;}
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 
 		this.uploads = [];
diff --git a/src/web/app/desktop/tags/analog-clock.tag b/src/web/app/desktop/tags/analog-clock.tag
index dda5a4b30..6b2bce3b2 100644
--- a/src/web/app/desktop/tags/analog-clock.tag
+++ b/src/web/app/desktop/tags/analog-clock.tag
@@ -7,7 +7,7 @@
 				width 256px
 				height 256px
 	</style>
-	<script>
+	<script lang="typescript">
 		const Vec2 = function(x, y) {
 			this.x = x;
 			this.y = y;
diff --git a/src/web/app/desktop/tags/autocomplete-suggestion.tag b/src/web/app/desktop/tags/autocomplete-suggestion.tag
index ec531a1b2..a0215666c 100644
--- a/src/web/app/desktop/tags/autocomplete-suggestion.tag
+++ b/src/web/app/desktop/tags/autocomplete-suggestion.tag
@@ -79,7 +79,7 @@
 						color rgba(0, 0, 0, 0.3)
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import contains from '../../common/scripts/contains';
 
 		this.mixin('api');
diff --git a/src/web/app/desktop/tags/big-follow-button.tag b/src/web/app/desktop/tags/big-follow-button.tag
index faac04a9f..6d43e4abe 100644
--- a/src/web/app/desktop/tags/big-follow-button.tag
+++ b/src/web/app/desktop/tags/big-follow-button.tag
@@ -73,7 +73,7 @@
 					opacity 0.7
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import isPromise from '../../common/scripts/is-promise';
 
 		this.mixin('i');
diff --git a/src/web/app/desktop/tags/contextmenu.tag b/src/web/app/desktop/tags/contextmenu.tag
index 09d989c09..67bdc5824 100644
--- a/src/web/app/desktop/tags/contextmenu.tag
+++ b/src/web/app/desktop/tags/contextmenu.tag
@@ -95,7 +95,7 @@
 				transition visibility 0s linear 0.2s
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import anime from 'animejs';
 		import contains from '../../common/scripts/contains';
 
diff --git a/src/web/app/desktop/tags/crop-window.tag b/src/web/app/desktop/tags/crop-window.tag
index 43bbcb8c5..1749986b2 100644
--- a/src/web/app/desktop/tags/crop-window.tag
+++ b/src/web/app/desktop/tags/crop-window.tag
@@ -159,7 +159,7 @@
 							width 150px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		const Cropper = require('cropperjs');
 
 		this.image = this.opts.file;
diff --git a/src/web/app/desktop/tags/detailed-post-window.tag b/src/web/app/desktop/tags/detailed-post-window.tag
index d5042612c..57e390d50 100644
--- a/src/web/app/desktop/tags/detailed-post-window.tag
+++ b/src/web/app/desktop/tags/detailed-post-window.tag
@@ -34,7 +34,7 @@
 					margin 0 auto
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import anime from 'animejs';
 
 		this.mixin('api');
diff --git a/src/web/app/desktop/tags/dialog.tag b/src/web/app/desktop/tags/dialog.tag
index 92ea0b2b1..cb8c0f31b 100644
--- a/src/web/app/desktop/tags/dialog.tag
+++ b/src/web/app/desktop/tags/dialog.tag
@@ -82,7 +82,7 @@
 							transition color 0s ease
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import anime from 'animejs';
 
 		this.canThrough = opts.canThrough != null ? opts.canThrough : true;
diff --git a/src/web/app/desktop/tags/donation.tag b/src/web/app/desktop/tags/donation.tag
index 8a711890f..fe446f2e6 100644
--- a/src/web/app/desktop/tags/donation.tag
+++ b/src/web/app/desktop/tags/donation.tag
@@ -46,7 +46,7 @@
 					margin-bottom 16px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 		this.mixin('api');
 
diff --git a/src/web/app/desktop/tags/drive/base-contextmenu.tag b/src/web/app/desktop/tags/drive/base-contextmenu.tag
index d2381cc47..f81526bef 100644
--- a/src/web/app/desktop/tags/drive/base-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/base-contextmenu.tag
@@ -12,7 +12,7 @@
 			</li>
 		</ul>
 	</mk-contextmenu>
-	<script>
+	<script lang="typescript">
 		this.browser = this.opts.browser;
 
 		this.on('mount', () => {
diff --git a/src/web/app/desktop/tags/drive/browser-window.tag b/src/web/app/desktop/tags/drive/browser-window.tag
index af225e00c..db7b89834 100644
--- a/src/web/app/desktop/tags/drive/browser-window.tag
+++ b/src/web/app/desktop/tags/drive/browser-window.tag
@@ -27,7 +27,7 @@
 						height 100%
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.folder = this.opts.folder ? this.opts.folder : null;
diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/tags/drive/browser.tag
index 9b9a42cc2..15c9bb569 100644
--- a/src/web/app/desktop/tags/drive/browser.tag
+++ b/src/web/app/desktop/tags/drive/browser.tag
@@ -242,7 +242,7 @@
 				display none
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import contains from '../../../common/scripts/contains';
 		import dialog from '../../scripts/dialog';
 		import inputDialog from '../../scripts/input-dialog';
diff --git a/src/web/app/desktop/tags/drive/file-contextmenu.tag b/src/web/app/desktop/tags/drive/file-contextmenu.tag
index bb934d35e..c7eeb01cd 100644
--- a/src/web/app/desktop/tags/drive/file-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/file-contextmenu.tag
@@ -34,7 +34,7 @@
 			</li>
 		</ul>
 	</mk-contextmenu>
-	<script>
+	<script lang="typescript">
 		import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
 		import dialog from '../../scripts/dialog';
 		import inputDialog from '../../scripts/input-dialog';
diff --git a/src/web/app/desktop/tags/drive/file.tag b/src/web/app/desktop/tags/drive/file.tag
index c55953cc7..a669f5fff 100644
--- a/src/web/app/desktop/tags/drive/file.tag
+++ b/src/web/app/desktop/tags/drive/file.tag
@@ -140,7 +140,7 @@
 					opacity 0.5
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import anime from 'animejs';
 		import bytesToSize from '../../../common/scripts/bytes-to-size';
 
diff --git a/src/web/app/desktop/tags/drive/folder-contextmenu.tag b/src/web/app/desktop/tags/drive/folder-contextmenu.tag
index 43cad3da5..d4c2f9380 100644
--- a/src/web/app/desktop/tags/drive/folder-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/folder-contextmenu.tag
@@ -17,7 +17,7 @@
 			</li>
 		</ul>
 	</mk-contextmenu>
-	<script>
+	<script lang="typescript">
 		import inputDialog from '../../scripts/input-dialog';
 
 		this.mixin('api');
diff --git a/src/web/app/desktop/tags/drive/folder.tag b/src/web/app/desktop/tags/drive/folder.tag
index 90d9f2b3c..1ba166a67 100644
--- a/src/web/app/desktop/tags/drive/folder.tag
+++ b/src/web/app/desktop/tags/drive/folder.tag
@@ -47,7 +47,7 @@
 					text-align left
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import dialog from '../../scripts/dialog';
 
 		this.mixin('api');
diff --git a/src/web/app/desktop/tags/drive/nav-folder.tag b/src/web/app/desktop/tags/drive/nav-folder.tag
index 9c943f26e..2afbb50f0 100644
--- a/src/web/app/desktop/tags/drive/nav-folder.tag
+++ b/src/web/app/desktop/tags/drive/nav-folder.tag
@@ -6,7 +6,7 @@
 				background #eee
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.folder = this.opts.folder ? this.opts.folder : null;
diff --git a/src/web/app/desktop/tags/follow-button.tag b/src/web/app/desktop/tags/follow-button.tag
index aa7e34321..843774ad0 100644
--- a/src/web/app/desktop/tags/follow-button.tag
+++ b/src/web/app/desktop/tags/follow-button.tag
@@ -70,7 +70,7 @@
 					opacity 0.7
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import isPromise from '../../common/scripts/is-promise';
 
 		this.mixin('i');
diff --git a/src/web/app/desktop/tags/following-setuper.tag b/src/web/app/desktop/tags/following-setuper.tag
index 8aeb8a3f0..75ce76ae5 100644
--- a/src/web/app/desktop/tags/following-setuper.tag
+++ b/src/web/app/desktop/tags/following-setuper.tag
@@ -120,7 +120,7 @@
 					padding 14px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 		this.mixin('user-preview');
 
diff --git a/src/web/app/desktop/tags/home-widgets/access-log.tag b/src/web/app/desktop/tags/home-widgets/access-log.tag
index 1e9ea0fdb..c3adc0d8b 100644
--- a/src/web/app/desktop/tags/home-widgets/access-log.tag
+++ b/src/web/app/desktop/tags/home-widgets/access-log.tag
@@ -47,7 +47,7 @@
 						margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import seedrandom from 'seedrandom';
 
 		this.data = {
diff --git a/src/web/app/desktop/tags/home-widgets/activity.tag b/src/web/app/desktop/tags/home-widgets/activity.tag
index 5cc542272..878de6d13 100644
--- a/src/web/app/desktop/tags/home-widgets/activity.tag
+++ b/src/web/app/desktop/tags/home-widgets/activity.tag
@@ -4,7 +4,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		this.data = {
 			view: 0,
 			design: 0
diff --git a/src/web/app/desktop/tags/home-widgets/broadcast.tag b/src/web/app/desktop/tags/home-widgets/broadcast.tag
index 963b31237..e1ba82e79 100644
--- a/src/web/app/desktop/tags/home-widgets/broadcast.tag
+++ b/src/web/app/desktop/tags/home-widgets/broadcast.tag
@@ -97,7 +97,7 @@
 				font-size 0.7em
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.data = {
 			design: 0
 		};
diff --git a/src/web/app/desktop/tags/home-widgets/calendar.tag b/src/web/app/desktop/tags/home-widgets/calendar.tag
index a304d6255..46d47662b 100644
--- a/src/web/app/desktop/tags/home-widgets/calendar.tag
+++ b/src/web/app/desktop/tags/home-widgets/calendar.tag
@@ -111,7 +111,7 @@
 							background #41ddde
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.data = {
 			design: 0
 		};
diff --git a/src/web/app/desktop/tags/home-widgets/channel.tag b/src/web/app/desktop/tags/home-widgets/channel.tag
index 3fc1f1abf..0b4fbbf4f 100644
--- a/src/web/app/desktop/tags/home-widgets/channel.tag
+++ b/src/web/app/desktop/tags/home-widgets/channel.tag
@@ -55,7 +55,7 @@
 				height 200px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.data = {
 			channel: null,
 			compact: false
@@ -137,7 +137,7 @@
 				bottom 0
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import ChannelStream from '../../../common/scripts/streaming/channel-stream';
 
 		this.mixin('api');
@@ -241,7 +241,7 @@
 							vertical-align bottom
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.post = this.opts.post;
 		this.form = this.opts.form;
 
@@ -275,7 +275,7 @@
 					border-color #aeaeae
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.clear = () => {
diff --git a/src/web/app/desktop/tags/home-widgets/donation.tag b/src/web/app/desktop/tags/home-widgets/donation.tag
index 327cae5a0..5ed5c137b 100644
--- a/src/web/app/desktop/tags/home-widgets/donation.tag
+++ b/src/web/app/desktop/tags/home-widgets/donation.tag
@@ -29,7 +29,7 @@
 					color #999
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('widget');
 		this.mixin('user-preview');
 	</script>
diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/tags/home-widgets/mentions.tag
index d4569216c..2ca1fa502 100644
--- a/src/web/app/desktop/tags/home-widgets/mentions.tag
+++ b/src/web/app/desktop/tags/home-widgets/mentions.tag
@@ -52,7 +52,7 @@
 					color #ccc
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 		this.mixin('api');
 
diff --git a/src/web/app/desktop/tags/home-widgets/messaging.tag b/src/web/app/desktop/tags/home-widgets/messaging.tag
index b5edd36fd..cd11c21a2 100644
--- a/src/web/app/desktop/tags/home-widgets/messaging.tag
+++ b/src/web/app/desktop/tags/home-widgets/messaging.tag
@@ -29,7 +29,7 @@
 				overflow auto
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.data = {
 			design: 0
 		};
diff --git a/src/web/app/desktop/tags/home-widgets/nav.tag b/src/web/app/desktop/tags/home-widgets/nav.tag
index 308652433..890fb4d8f 100644
--- a/src/web/app/desktop/tags/home-widgets/nav.tag
+++ b/src/web/app/desktop/tags/home-widgets/nav.tag
@@ -17,7 +17,7 @@
 				color #ccc
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('widget');
 	</script>
 </mk-nav-home-widget>
diff --git a/src/web/app/desktop/tags/home-widgets/notifications.tag b/src/web/app/desktop/tags/home-widgets/notifications.tag
index 4a6d7b417..4c48da659 100644
--- a/src/web/app/desktop/tags/home-widgets/notifications.tag
+++ b/src/web/app/desktop/tags/home-widgets/notifications.tag
@@ -46,7 +46,7 @@
 				overflow auto
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.data = {
 			compact: false
 		};
diff --git a/src/web/app/desktop/tags/home-widgets/photo-stream.tag b/src/web/app/desktop/tags/home-widgets/photo-stream.tag
index 6040e4611..8c57dbbef 100644
--- a/src/web/app/desktop/tags/home-widgets/photo-stream.tag
+++ b/src/web/app/desktop/tags/home-widgets/photo-stream.tag
@@ -69,7 +69,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.data = {
 			design: 0
 		};
diff --git a/src/web/app/desktop/tags/home-widgets/post-form.tag b/src/web/app/desktop/tags/home-widgets/post-form.tag
index a3dc3dd6e..58ceac604 100644
--- a/src/web/app/desktop/tags/home-widgets/post-form.tag
+++ b/src/web/app/desktop/tags/home-widgets/post-form.tag
@@ -62,7 +62,7 @@
 					transition background 0s ease
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.data = {
 			design: 0
 		};
diff --git a/src/web/app/desktop/tags/home-widgets/profile.tag b/src/web/app/desktop/tags/home-widgets/profile.tag
index 30ca3c3b6..02a1f0d5a 100644
--- a/src/web/app/desktop/tags/home-widgets/profile.tag
+++ b/src/web/app/desktop/tags/home-widgets/profile.tag
@@ -87,7 +87,7 @@
 				color #999
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import inputDialog from '../../scripts/input-dialog';
 		import updateAvatar from '../../scripts/update-avatar';
 		import updateBanner from '../../scripts/update-banner';
diff --git a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag b/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
index cf76ea9c1..f33b2de5f 100644
--- a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
+++ b/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
@@ -73,7 +73,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.data = {
 			compact: false
 		};
diff --git a/src/web/app/desktop/tags/home-widgets/rss-reader.tag b/src/web/app/desktop/tags/home-widgets/rss-reader.tag
index 916281def..f8a0787d3 100644
--- a/src/web/app/desktop/tags/home-widgets/rss-reader.tag
+++ b/src/web/app/desktop/tags/home-widgets/rss-reader.tag
@@ -65,7 +65,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.data = {
 			compact: false
 		};
diff --git a/src/web/app/desktop/tags/home-widgets/server.tag b/src/web/app/desktop/tags/home-widgets/server.tag
index cae2306a5..1a15d3704 100644
--- a/src/web/app/desktop/tags/home-widgets/server.tag
+++ b/src/web/app/desktop/tags/home-widgets/server.tag
@@ -61,7 +61,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('os');
 
 		this.data = {
@@ -186,7 +186,7 @@
 				display block
 				clear both
 	</style>
-	<script>
+	<script lang="typescript">
 		import uuid from 'uuid';
 
 		this.viewBoxX = 50;
@@ -270,7 +270,7 @@
 				clear both
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.cores = this.opts.meta.cpu.cores;
 		this.model = this.opts.meta.cpu.model;
 		this.connection = this.opts.connection;
@@ -328,7 +328,7 @@
 				clear both
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import bytesToSize from '../../../common/scripts/bytes-to-size';
 
 		this.connection = this.opts.connection;
@@ -394,7 +394,7 @@
 				clear both
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import bytesToSize from '../../../common/scripts/bytes-to-size';
 
 		this.connection = this.opts.connection;
@@ -440,7 +440,7 @@
 					font-weight bold
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.connection = this.opts.connection;
 
 		this.on('mount', () => {
@@ -475,7 +475,7 @@
 				color #505050
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.meta = this.opts.meta;
 	</script>
 </mk-server-home-widget-info>
@@ -516,7 +516,7 @@
 					fill rgba(0, 0, 0, 0.6)
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.r = 0.4;
 
 		this.render = p => {
diff --git a/src/web/app/desktop/tags/home-widgets/slideshow.tag b/src/web/app/desktop/tags/home-widgets/slideshow.tag
index ab78ca2c6..817b138d3 100644
--- a/src/web/app/desktop/tags/home-widgets/slideshow.tag
+++ b/src/web/app/desktop/tags/home-widgets/slideshow.tag
@@ -48,7 +48,7 @@
 						opacity 0
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import anime from 'animejs';
 
 		this.data = {
diff --git a/src/web/app/desktop/tags/home-widgets/timeline.tag b/src/web/app/desktop/tags/home-widgets/timeline.tag
index 2bbee14fa..67e56b676 100644
--- a/src/web/app/desktop/tags/home-widgets/timeline.tag
+++ b/src/web/app/desktop/tags/home-widgets/timeline.tag
@@ -38,7 +38,7 @@
 					color #ccc
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 		this.mixin('api');
 
diff --git a/src/web/app/desktop/tags/home-widgets/timemachine.tag b/src/web/app/desktop/tags/home-widgets/timemachine.tag
index e47ce2d4a..43f59fe67 100644
--- a/src/web/app/desktop/tags/home-widgets/timemachine.tag
+++ b/src/web/app/desktop/tags/home-widgets/timemachine.tag
@@ -4,7 +4,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		this.data = {
 			design: 0
 		};
diff --git a/src/web/app/desktop/tags/home-widgets/tips.tag b/src/web/app/desktop/tags/home-widgets/tips.tag
index 2135a836c..a352253ce 100644
--- a/src/web/app/desktop/tags/home-widgets/tips.tag
+++ b/src/web/app/desktop/tags/home-widgets/tips.tag
@@ -26,7 +26,7 @@
 					border-radius 2px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import anime from 'animejs';
 
 		this.mixin('widget');
diff --git a/src/web/app/desktop/tags/home-widgets/trends.tag b/src/web/app/desktop/tags/home-widgets/trends.tag
index db2ed9510..4e5060a3e 100644
--- a/src/web/app/desktop/tags/home-widgets/trends.tag
+++ b/src/web/app/desktop/tags/home-widgets/trends.tag
@@ -75,7 +75,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.data = {
 			compact: false
 		};
diff --git a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
index 25a60b95a..fb23eac5e 100644
--- a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
+++ b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
@@ -114,7 +114,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.data = {
 			compact: false
 		};
diff --git a/src/web/app/desktop/tags/home-widgets/version.tag b/src/web/app/desktop/tags/home-widgets/version.tag
index aeebb53b0..6dd8ad644 100644
--- a/src/web/app/desktop/tags/home-widgets/version.tag
+++ b/src/web/app/desktop/tags/home-widgets/version.tag
@@ -14,7 +14,7 @@
 				color #aaa
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('widget');
 	</script>
 </mk-version-home-widget>
diff --git a/src/web/app/desktop/tags/home.tag b/src/web/app/desktop/tags/home.tag
index f727c3e80..827622930 100644
--- a/src/web/app/desktop/tags/home.tag
+++ b/src/web/app/desktop/tags/home.tag
@@ -180,7 +180,7 @@
 						margin 0 auto
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import uuid from 'uuid';
 		import Sortable from 'sortablejs';
 		import dialog from '../scripts/dialog';
diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
index 088f937e7..594c706be 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -8,7 +8,7 @@
 			grid-gap 4px
 			height 256px
 	</style>
-	<script>
+	<script lang="typescript">
 		this.images = this.opts.images;
 
 		this.on('mount', () => {
@@ -78,7 +78,7 @@
 					background-size cover
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.image = this.opts.image;
 		this.styles = {
 			'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
@@ -145,7 +145,7 @@
 				cursor zoom-out
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import anime from 'animejs';
 
 		this.image = this.opts.image;
diff --git a/src/web/app/desktop/tags/input-dialog.tag b/src/web/app/desktop/tags/input-dialog.tag
index 26fa384e6..a1634429c 100644
--- a/src/web/app/desktop/tags/input-dialog.tag
+++ b/src/web/app/desktop/tags/input-dialog.tag
@@ -119,7 +119,7 @@
 								border-color #dcdcdc
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.done = false;
 
 		this.title = this.opts.title;
diff --git a/src/web/app/desktop/tags/list-user.tag b/src/web/app/desktop/tags/list-user.tag
index 45c4deb53..bde90b1cc 100644
--- a/src/web/app/desktop/tags/list-user.tag
+++ b/src/web/app/desktop/tags/list-user.tag
@@ -89,5 +89,5 @@
 				right 16px
 
 	</style>
-	<script>this.user = this.opts.user</script>
+	<script lang="typescript">this.user = this.opts.user</script>
 </mk-list-user>
diff --git a/src/web/app/desktop/tags/messaging/room-window.tag b/src/web/app/desktop/tags/messaging/room-window.tag
index b13c2d3e9..ca1187364 100644
--- a/src/web/app/desktop/tags/messaging/room-window.tag
+++ b/src/web/app/desktop/tags/messaging/room-window.tag
@@ -18,7 +18,7 @@
 						overflow auto
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.user = this.opts.user;
 
 		this.popout = `${_URL_}/i/messaging/${this.user.username}`;
diff --git a/src/web/app/desktop/tags/messaging/window.tag b/src/web/app/desktop/tags/messaging/window.tag
index ac5513a3f..e078bccad 100644
--- a/src/web/app/desktop/tags/messaging/window.tag
+++ b/src/web/app/desktop/tags/messaging/window.tag
@@ -18,7 +18,7 @@
 						overflow auto
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.on('mount', () => {
 			this.$refs.window.on('closed', () => {
 				this.$destroy();
diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag
index 6a16db135..7bba90a8b 100644
--- a/src/web/app/desktop/tags/notifications.tag
+++ b/src/web/app/desktop/tags/notifications.tag
@@ -214,7 +214,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import getPostSummary from '../../../../common/get-post-summary.ts';
 		this.getPostSummary = getPostSummary;
 
diff --git a/src/web/app/desktop/tags/pages/drive.tag b/src/web/app/desktop/tags/pages/drive.tag
index 12ebcc47c..f4e2a3740 100644
--- a/src/web/app/desktop/tags/pages/drive.tag
+++ b/src/web/app/desktop/tags/pages/drive.tag
@@ -11,7 +11,7 @@
 			> mk-drive-browser
 				height 100%
 	</style>
-	<script>
+	<script lang="typescript">
 		this.on('mount', () => {
 			document.title = 'Misskey Drive';
 
diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/tags/pages/entrance.tag
index c516bdb38..56cec3490 100644
--- a/src/web/app/desktop/tags/pages/entrance.tag
+++ b/src/web/app/desktop/tags/pages/entrance.tag
@@ -107,7 +107,7 @@
 						font-size 10px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.mode = 'signin';
@@ -278,7 +278,7 @@
 				color #666
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.on('mount', () => {
 			this.$refs.signin.on('user', user => {
 				this.update({
diff --git a/src/web/app/desktop/tags/pages/home-customize.tag b/src/web/app/desktop/tags/pages/home-customize.tag
index ad74e095d..178558f9d 100644
--- a/src/web/app/desktop/tags/pages/home-customize.tag
+++ b/src/web/app/desktop/tags/pages/home-customize.tag
@@ -4,7 +4,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		this.on('mount', () => {
 			document.title = 'Misskey - ホームのカスタマイズ';
 		});
diff --git a/src/web/app/desktop/tags/pages/home.tag b/src/web/app/desktop/tags/pages/home.tag
index 206592518..9b9d455b5 100644
--- a/src/web/app/desktop/tags/pages/home.tag
+++ b/src/web/app/desktop/tags/pages/home.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import Progress from '../../../common/scripts/loading';
 		import getPostSummary from '../../../../../common/get-post-summary.ts';
 
diff --git a/src/web/app/desktop/tags/pages/messaging-room.tag b/src/web/app/desktop/tags/pages/messaging-room.tag
index 54bd38e57..bfa8c2465 100644
--- a/src/web/app/desktop/tags/pages/messaging-room.tag
+++ b/src/web/app/desktop/tags/pages/messaging-room.tag
@@ -7,7 +7,7 @@
 			background #fff
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import Progress from '../../../common/scripts/loading';
 
 		this.mixin('api');
diff --git a/src/web/app/desktop/tags/pages/post.tag b/src/web/app/desktop/tags/pages/post.tag
index b5cfea3ad..488adc6e3 100644
--- a/src/web/app/desktop/tags/pages/post.tag
+++ b/src/web/app/desktop/tags/pages/post.tag
@@ -31,7 +31,7 @@
 					width 640px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import Progress from '../../../common/scripts/loading';
 
 		this.mixin('api');
diff --git a/src/web/app/desktop/tags/pages/search.tag b/src/web/app/desktop/tags/pages/search.tag
index 4d72fad65..eaa80a039 100644
--- a/src/web/app/desktop/tags/pages/search.tag
+++ b/src/web/app/desktop/tags/pages/search.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import Progress from '../../../common/scripts/loading';
 
 		this.on('mount', () => {
diff --git a/src/web/app/desktop/tags/pages/selectdrive.tag b/src/web/app/desktop/tags/pages/selectdrive.tag
index 723a1dd5a..dd4d30f41 100644
--- a/src/web/app/desktop/tags/pages/selectdrive.tag
+++ b/src/web/app/desktop/tags/pages/selectdrive.tag
@@ -126,7 +126,7 @@
 						border-color #dcdcdc
 
 	</style>
-	<script>
+	<script lang="typescript">
 		const q = (new URL(location)).searchParams;
 		this.multiple = q.get('multiple') == 'true' ? true : false;
 
diff --git a/src/web/app/desktop/tags/pages/user.tag b/src/web/app/desktop/tags/pages/user.tag
index 8ea47408c..abed2ef02 100644
--- a/src/web/app/desktop/tags/pages/user.tag
+++ b/src/web/app/desktop/tags/pages/user.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import Progress from '../../../common/scripts/loading';
 
 		this.user = this.opts.user;
diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/tags/post-detail-sub.tag
index 0b8d4d1d3..208805670 100644
--- a/src/web/app/desktop/tags/post-detail-sub.tag
+++ b/src/web/app/desktop/tags/post-detail-sub.tag
@@ -106,7 +106,7 @@
 							margin-top 8px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import compile from '../../common/scripts/text-compiler';
 		import dateStringify from '../../common/scripts/date-stringify';
 
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index a4f88da7d..34b34b6a5 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -236,7 +236,7 @@
 						border-top 1px solid #eef0f2
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import compile from '../../common/scripts/text-compiler';
 		import dateStringify from '../../common/scripts/date-stringify';
 
diff --git a/src/web/app/desktop/tags/post-form-window.tag b/src/web/app/desktop/tags/post-form-window.tag
index 80b51df60..562621bde 100644
--- a/src/web/app/desktop/tags/post-form-window.tag
+++ b/src/web/app/desktop/tags/post-form-window.tag
@@ -37,7 +37,7 @@
 							margin 16px 22px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.uploadingFiles = [];
 		this.files = [];
 
diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag
index e4a9800cf..c2da85885 100644
--- a/src/web/app/desktop/tags/post-form.tag
+++ b/src/web/app/desktop/tags/post-form.tag
@@ -282,7 +282,7 @@
 				pointer-events none
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import Sortable from 'sortablejs';
 		import getKao from '../../common/scripts/get-kao';
 		import notify from '../scripts/notify';
diff --git a/src/web/app/desktop/tags/post-preview.tag b/src/web/app/desktop/tags/post-preview.tag
index dcad0ff7c..eb71e5e87 100644
--- a/src/web/app/desktop/tags/post-preview.tag
+++ b/src/web/app/desktop/tags/post-preview.tag
@@ -82,7 +82,7 @@
 							color #717171
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import dateStringify from '../../common/scripts/date-stringify';
 
 		this.mixin('user-preview');
diff --git a/src/web/app/desktop/tags/progress-dialog.tag b/src/web/app/desktop/tags/progress-dialog.tag
index 2359802be..5df5d7f57 100644
--- a/src/web/app/desktop/tags/progress-dialog.tag
+++ b/src/web/app/desktop/tags/progress-dialog.tag
@@ -72,7 +72,7 @@
 								to   {background-position: -64px 32px;}
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.title = this.opts.title;
 		this.value = parseInt(this.opts.value, 10);
 		this.max = parseInt(this.opts.max, 10);
diff --git a/src/web/app/desktop/tags/repost-form-window.tag b/src/web/app/desktop/tags/repost-form-window.tag
index 13a862d97..25f509c62 100644
--- a/src/web/app/desktop/tags/repost-form-window.tag
+++ b/src/web/app/desktop/tags/repost-form-window.tag
@@ -15,7 +15,7 @@
 						margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.onDocumentKeydown = e => {
 			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
 				if (e.which == 27) { // Esc
diff --git a/src/web/app/desktop/tags/repost-form.tag b/src/web/app/desktop/tags/repost-form.tag
index 06ee32150..77118124c 100644
--- a/src/web/app/desktop/tags/repost-form.tag
+++ b/src/web/app/desktop/tags/repost-form.tag
@@ -84,7 +84,7 @@
 						border-color $theme-color
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import notify from '../scripts/notify';
 
 		this.mixin('api');
diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag
index 3343697ca..09320c5d7 100644
--- a/src/web/app/desktop/tags/search-posts.tag
+++ b/src/web/app/desktop/tags/search-posts.tag
@@ -32,7 +32,7 @@
 					color #ccc
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import parse from '../../common/scripts/parse-search-query';
 
 		this.mixin('api');
diff --git a/src/web/app/desktop/tags/search.tag b/src/web/app/desktop/tags/search.tag
index 492999181..ec6bbfc34 100644
--- a/src/web/app/desktop/tags/search.tag
+++ b/src/web/app/desktop/tags/search.tag
@@ -22,7 +22,7 @@
 				overflow hidden
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.query = this.opts.query;
 
 		this.on('mount', () => {
diff --git a/src/web/app/desktop/tags/select-file-from-drive-window.tag b/src/web/app/desktop/tags/select-file-from-drive-window.tag
index f776f0ecb..10dc7db9f 100644
--- a/src/web/app/desktop/tags/select-file-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-file-from-drive-window.tag
@@ -134,7 +134,7 @@
 								border-color #dcdcdc
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.files = [];
 
 		this.multiple = this.opts.multiple != null ? this.opts.multiple : false;
diff --git a/src/web/app/desktop/tags/select-folder-from-drive-window.tag b/src/web/app/desktop/tags/select-folder-from-drive-window.tag
index 317fb90ad..1cd7527c8 100644
--- a/src/web/app/desktop/tags/select-folder-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-folder-from-drive-window.tag
@@ -89,7 +89,7 @@
 								border-color #dcdcdc
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.files = [];
 
 		this.title = this.opts.title || '%fa:R folder%フォルダを選択';
diff --git a/src/web/app/desktop/tags/set-avatar-suggestion.tag b/src/web/app/desktop/tags/set-avatar-suggestion.tag
index 923871a79..e67a8c66d 100644
--- a/src/web/app/desktop/tags/set-avatar-suggestion.tag
+++ b/src/web/app/desktop/tags/set-avatar-suggestion.tag
@@ -30,7 +30,7 @@
 					color #fff
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import updateAvatar from '../scripts/update-avatar';
 
 		this.mixin('i');
diff --git a/src/web/app/desktop/tags/set-banner-suggestion.tag b/src/web/app/desktop/tags/set-banner-suggestion.tag
index fa4e5843b..0d32c9a0e 100644
--- a/src/web/app/desktop/tags/set-banner-suggestion.tag
+++ b/src/web/app/desktop/tags/set-banner-suggestion.tag
@@ -30,7 +30,7 @@
 					color #fff
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import updateBanner from '../scripts/update-banner';
 
 		this.mixin('i');
diff --git a/src/web/app/desktop/tags/settings-window.tag b/src/web/app/desktop/tags/settings-window.tag
index 64ce1336d..094225f61 100644
--- a/src/web/app/desktop/tags/settings-window.tag
+++ b/src/web/app/desktop/tags/settings-window.tag
@@ -16,7 +16,7 @@
 					overflow hidden
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.on('mount', () => {
 			this.$refs.window.on('closed', () => {
 				this.$destroy();
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 1e3097ba1..3288ba721 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -119,7 +119,7 @@
 						border-bottom solid 1px #eee
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.page = 'profile';
 
 		this.setPage = page => {
@@ -166,7 +166,7 @@
 					margin-left 8px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import updateAvatar from '../scripts/update-avatar';
 		import notify from '../scripts/notify';
 
@@ -208,7 +208,7 @@
 				background #eee
 				border-radius 2px
 	</style>
-	<script>
+	<script lang="typescript">
 		import passwordDialog from '../scripts/password-dialog';
 
 		this.mixin('i');
@@ -231,7 +231,7 @@
 			display block
 			color #4a535a
 	</style>
-	<script>
+	<script lang="typescript">
 		import passwordDialog from '../scripts/password-dialog';
 		import dialog from '../scripts/dialog';
 		import notify from '../scripts/notify';
@@ -287,7 +287,7 @@
 			color #4a535a
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import passwordDialog from '../scripts/password-dialog';
 		import notify from '../scripts/notify';
 
@@ -370,7 +370,7 @@
 					fill rgba(0, 0, 0, 0.6)
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.r = 0.4;
@@ -408,7 +408,7 @@
 			display block
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.apps = [];
diff --git a/src/web/app/desktop/tags/sub-post-content.tag b/src/web/app/desktop/tags/sub-post-content.tag
index 184fc53eb..40b3b3005 100644
--- a/src/web/app/desktop/tags/sub-post-content.tag
+++ b/src/web/app/desktop/tags/sub-post-content.tag
@@ -33,7 +33,7 @@
 				font-size 80%
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import compile from '../../common/scripts/text-compiler';
 
 		this.mixin('user-preview');
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 98970bfa1..485353346 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -35,7 +35,7 @@
 				border-bottom-right-radius 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.posts = [];
 
 		this.on('update', () => {
@@ -409,7 +409,7 @@
 				background rgba(0, 0, 0, 0.0125)
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import compile from '../../common/scripts/text-compiler';
 		import dateStringify from '../../common/scripts/date-stringify';
 
@@ -693,7 +693,7 @@
 								font-size 80%
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import dateStringify from '../../common/scripts/date-stringify';
 
 		this.mixin('user-preview');
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index a8ddcaf93..0a3849236 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -10,7 +10,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 
 		this.openPostForm = () => {
@@ -119,7 +119,7 @@
 									display none
 
 	</style>
-	<script>this.mixin('i');</script>
+	<script lang="typescript">this.mixin('i');</script>
 </mk-ui-header>
 
 <mk-ui-header-search>
@@ -175,7 +175,7 @@
 						box-shadow 0 0 0 2px rgba($theme-color, 0.5) !important
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('page');
 
 		this.onsubmit = e => {
@@ -221,7 +221,7 @@
 					transition background 0s ease
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.post = e => {
 			this.parent.parent.openPostForm();
 		};
@@ -310,7 +310,7 @@
 					overflow auto
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import contains from '../../common/scripts/contains';
 
 		this.mixin('i');
@@ -487,7 +487,7 @@
 							padding 0 12px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 		this.mixin('api');
 
@@ -604,7 +604,7 @@
 				background #899492
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.now = new Date();
 
 		this.draw = () => {
@@ -789,7 +789,7 @@
 								color $theme-color-foreground
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import contains from '../../common/scripts/contains';
 		import signout from '../../common/scripts/signout';
 		this.signout = signout;
@@ -869,7 +869,7 @@
 				text-align center
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import anime from 'animejs';
 
 		this.on('mount', () => {
diff --git a/src/web/app/desktop/tags/user-followers-window.tag b/src/web/app/desktop/tags/user-followers-window.tag
index a67888fa7..82bec6992 100644
--- a/src/web/app/desktop/tags/user-followers-window.tag
+++ b/src/web/app/desktop/tags/user-followers-window.tag
@@ -15,5 +15,5 @@
 						border-radius 4px
 
 	</style>
-	<script>this.user = this.opts.user</script>
+	<script lang="typescript">this.user = this.opts.user</script>
 </mk-user-followers-window>
diff --git a/src/web/app/desktop/tags/user-followers.tag b/src/web/app/desktop/tags/user-followers.tag
index 79fa87141..a1b44f0f5 100644
--- a/src/web/app/desktop/tags/user-followers.tag
+++ b/src/web/app/desktop/tags/user-followers.tag
@@ -6,7 +6,7 @@
 			height 100%
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.user = this.opts.user;
diff --git a/src/web/app/desktop/tags/user-following-window.tag b/src/web/app/desktop/tags/user-following-window.tag
index dd798a020..0f1c4b3ea 100644
--- a/src/web/app/desktop/tags/user-following-window.tag
+++ b/src/web/app/desktop/tags/user-following-window.tag
@@ -15,5 +15,5 @@
 						border-radius 4px
 
 	</style>
-	<script>this.user = this.opts.user</script>
+	<script lang="typescript">this.user = this.opts.user</script>
 </mk-user-following-window>
diff --git a/src/web/app/desktop/tags/user-following.tag b/src/web/app/desktop/tags/user-following.tag
index 260900f95..db46bf110 100644
--- a/src/web/app/desktop/tags/user-following.tag
+++ b/src/web/app/desktop/tags/user-following.tag
@@ -6,7 +6,7 @@
 			height 100%
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.user = this.opts.user;
diff --git a/src/web/app/desktop/tags/user-preview.tag b/src/web/app/desktop/tags/user-preview.tag
index eb3568ce0..00ecfba1b 100644
--- a/src/web/app/desktop/tags/user-preview.tag
+++ b/src/web/app/desktop/tags/user-preview.tag
@@ -98,7 +98,7 @@
 				right 8px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import anime from 'animejs';
 
 		this.mixin('i');
diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/tags/user-timeline.tag
index 427ce9c53..3baf5db0e 100644
--- a/src/web/app/desktop/tags/user-timeline.tag
+++ b/src/web/app/desktop/tags/user-timeline.tag
@@ -52,7 +52,7 @@
 					color #ccc
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import isPromise from '../../common/scripts/is-promise';
 
 		this.mixin('api');
diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/tags/user.tag
index 364b95ba7..daf39347f 100644
--- a/src/web/app/desktop/tags/user.tag
+++ b/src/web/app/desktop/tags/user.tag
@@ -16,7 +16,7 @@
 						overflow hidden
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.username = this.opts.user;
@@ -182,7 +182,7 @@
 							border solid 1px #ddd
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import updateBanner from '../scripts/update-banner';
 
 		this.mixin('i');
@@ -309,7 +309,7 @@
 						margin-right 8px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.age = require('s-age');
 
 		this.mixin('i');
@@ -411,7 +411,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import isPromise from '../../common/scripts/is-promise';
 
 		this.mixin('api');
@@ -539,7 +539,7 @@
 					right 16px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.user = this.opts.user;
@@ -612,7 +612,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.user = this.opts.user;
@@ -707,7 +707,7 @@
 							color #ccc
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import ScrollFollower from '../scripts/scroll-follower';
 
 		this.mixin('i');
@@ -776,7 +776,7 @@
 							margin-right 8px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.on('mount', () => {
 			this.trigger('loaded');
 		});
@@ -819,7 +819,7 @@
 					transform-origin center
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import getMedian from '../../common/scripts/get-median';
 
 		this.mixin('api');
diff --git a/src/web/app/desktop/tags/users-list.tag b/src/web/app/desktop/tags/users-list.tag
index 18ba2b77d..90173bfd2 100644
--- a/src/web/app/desktop/tags/users-list.tag
+++ b/src/web/app/desktop/tags/users-list.tag
@@ -88,7 +88,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 
 		this.limit = 30;
diff --git a/src/web/app/desktop/tags/widgets/activity.tag b/src/web/app/desktop/tags/widgets/activity.tag
index 8aad5337f..03d253ea2 100644
--- a/src/web/app/desktop/tags/widgets/activity.tag
+++ b/src/web/app/desktop/tags/widgets/activity.tag
@@ -57,7 +57,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.design = this.opts.design || 0;
@@ -127,7 +127,7 @@
 							fill rgba(0, 0, 0, 0.05)
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.data = this.opts.data;
 		this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
 		const peak = Math.max.apply(null, this.data.map(d => d.total));
@@ -184,7 +184,7 @@
 				width 100%
 				cursor all-scroll
 	</style>
-	<script>
+	<script lang="typescript">
 		this.viewBoxX = 140;
 		this.viewBoxY = 60;
 		this.zoom = 1;
diff --git a/src/web/app/desktop/tags/widgets/calendar.tag b/src/web/app/desktop/tags/widgets/calendar.tag
index c8d268783..3d2d84e40 100644
--- a/src/web/app/desktop/tags/widgets/calendar.tag
+++ b/src/web/app/desktop/tags/widgets/calendar.tag
@@ -137,7 +137,7 @@
 								background darken($theme-color, 10%)
 
 	</style>
-	<script>
+	<script lang="typescript">
 		if (this.opts.design == null) this.opts.design = 0;
 
 		const eachMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
diff --git a/src/web/app/desktop/tags/window.tag b/src/web/app/desktop/tags/window.tag
index 2b98ab7f0..dc7a37fff 100644
--- a/src/web/app/desktop/tags/window.tag
+++ b/src/web/app/desktop/tags/window.tag
@@ -185,7 +185,7 @@
 					height calc(100% - 40px)
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import anime from 'animejs';
 		import contains from '../../common/scripts/contains';
 
diff --git a/src/web/app/dev/tags/new-app-form.tag b/src/web/app/dev/tags/new-app-form.tag
index f753b5ae3..672c31570 100644
--- a/src/web/app/dev/tags/new-app-form.tag
+++ b/src/web/app/dev/tags/new-app-form.tag
@@ -177,7 +177,7 @@
 					border-radius 3px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.nidState = null;
diff --git a/src/web/app/dev/tags/pages/app.tag b/src/web/app/dev/tags/pages/app.tag
index 1e89b47d8..42937a21b 100644
--- a/src/web/app/dev/tags/pages/app.tag
+++ b/src/web/app/dev/tags/pages/app.tag
@@ -13,7 +13,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.fetching = true;
diff --git a/src/web/app/dev/tags/pages/apps.tag b/src/web/app/dev/tags/pages/apps.tag
index d11011ca4..f7b8e416e 100644
--- a/src/web/app/dev/tags/pages/apps.tag
+++ b/src/web/app/dev/tags/pages/apps.tag
@@ -14,7 +14,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.fetching = true;
diff --git a/src/web/app/mobile/tags/drive-folder-selector.tag b/src/web/app/mobile/tags/drive-folder-selector.tag
index 94cf1db41..a63d90af5 100644
--- a/src/web/app/mobile/tags/drive-folder-selector.tag
+++ b/src/web/app/mobile/tags/drive-folder-selector.tag
@@ -55,7 +55,7 @@
 					-webkit-overflow-scrolling touch
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.cancel = () => {
 			this.trigger('canceled');
 			this.$destroy();
diff --git a/src/web/app/mobile/tags/drive-selector.tag b/src/web/app/mobile/tags/drive-selector.tag
index a837f8b5f..d3e4f54c2 100644
--- a/src/web/app/mobile/tags/drive-selector.tag
+++ b/src/web/app/mobile/tags/drive-selector.tag
@@ -59,7 +59,7 @@
 					-webkit-overflow-scrolling touch
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.files = [];
 
 		this.on('mount', () => {
diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index 0076dc8f4..b5e428665 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -172,7 +172,7 @@
 				display none
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 		this.mixin('api');
 
diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag
index 5d06507c4..846d12d86 100644
--- a/src/web/app/mobile/tags/drive/file-viewer.tag
+++ b/src/web/app/mobile/tags/drive/file-viewer.tag
@@ -227,7 +227,7 @@
 						background #f5f5f5
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import EXIF from 'exif-js';
 		import hljs from 'highlight.js';
 		import bytesToSize from '../../../common/scripts/bytes-to-size';
diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag
index 03cbab2bf..8afac7982 100644
--- a/src/web/app/mobile/tags/drive/file.tag
+++ b/src/web/app/mobile/tags/drive/file.tag
@@ -126,7 +126,7 @@
 					color #fff !important
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import bytesToSize from '../../../common/scripts/bytes-to-size';
 		this.bytesToSize = bytesToSize;
 
diff --git a/src/web/app/mobile/tags/drive/folder.tag b/src/web/app/mobile/tags/drive/folder.tag
index bb17c5e67..2fe6c2c39 100644
--- a/src/web/app/mobile/tags/drive/folder.tag
+++ b/src/web/app/mobile/tags/drive/folder.tag
@@ -40,7 +40,7 @@
 							height 100%
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.browser = this.parent;
 		this.folder = this.opts.folder;
 
diff --git a/src/web/app/mobile/tags/follow-button.tag b/src/web/app/mobile/tags/follow-button.tag
index d96389bfc..bd4ecbaf9 100644
--- a/src/web/app/mobile/tags/follow-button.tag
+++ b/src/web/app/mobile/tags/follow-button.tag
@@ -51,7 +51,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import isPromise from '../../common/scripts/is-promise';
 
 		this.mixin('i');
diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag
index 3905e867b..70074ef9f 100644
--- a/src/web/app/mobile/tags/home-timeline.tag
+++ b/src/web/app/mobile/tags/home-timeline.tag
@@ -9,7 +9,7 @@
 				margin-bottom 8px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 		this.mixin('api');
 
diff --git a/src/web/app/mobile/tags/home.tag b/src/web/app/mobile/tags/home.tag
index 1bb9027dd..a304708b3 100644
--- a/src/web/app/mobile/tags/home.tag
+++ b/src/web/app/mobile/tags/home.tag
@@ -13,7 +13,7 @@
 				padding 16px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.on('mount', () => {
 			this.$refs.tl.on('loaded', () => {
 				this.trigger('loaded');
diff --git a/src/web/app/mobile/tags/images.tag b/src/web/app/mobile/tags/images.tag
index c39eda38b..f4a103311 100644
--- a/src/web/app/mobile/tags/images.tag
+++ b/src/web/app/mobile/tags/images.tag
@@ -11,7 +11,7 @@
 			@media (max-width 500px)
 				height 192px
 	</style>
-	<script>
+	<script lang="typescript">
 		this.images = this.opts.images;
 
 		this.on('mount', () => {
@@ -72,7 +72,7 @@
 				background-size cover
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.image = this.opts.image;
 		this.styles = {
 			'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
diff --git a/src/web/app/mobile/tags/init-following.tag b/src/web/app/mobile/tags/init-following.tag
index 3eb3e1481..94949a2e2 100644
--- a/src/web/app/mobile/tags/init-following.tag
+++ b/src/web/app/mobile/tags/init-following.tag
@@ -82,7 +82,7 @@
 					padding 10px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.users = null;
diff --git a/src/web/app/mobile/tags/notification-preview.tag b/src/web/app/mobile/tags/notification-preview.tag
index a24110086..06f4fb511 100644
--- a/src/web/app/mobile/tags/notification-preview.tag
+++ b/src/web/app/mobile/tags/notification-preview.tag
@@ -102,7 +102,7 @@
 					color #fff
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import getPostSummary from '../../../../common/get-post-summary.ts';
 		this.getPostSummary = getPostSummary;
 		this.notification = this.opts.notification;
diff --git a/src/web/app/mobile/tags/notification.tag b/src/web/app/mobile/tags/notification.tag
index 977244e0c..9aca50cb4 100644
--- a/src/web/app/mobile/tags/notification.tag
+++ b/src/web/app/mobile/tags/notification.tag
@@ -161,7 +161,7 @@
 					color rgba(0, 0, 0, 0.7)
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import getPostSummary from '../../../../common/get-post-summary.ts';
 		this.getPostSummary = getPostSummary;
 		this.notification = this.opts.notification;
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index d1a6a2501..2ff961ae2 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -77,7 +77,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import getPostSummary from '../../../../common/get-post-summary.ts';
 		this.getPostSummary = getPostSummary;
 
diff --git a/src/web/app/mobile/tags/notify.tag b/src/web/app/mobile/tags/notify.tag
index 787d3a374..59d1e9dd8 100644
--- a/src/web/app/mobile/tags/notify.tag
+++ b/src/web/app/mobile/tags/notify.tag
@@ -15,7 +15,7 @@
 			background-color rgba(#000, 0.5)
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import anime from 'animejs';
 
 		this.on('mount', () => {
diff --git a/src/web/app/mobile/tags/page/drive.tag b/src/web/app/mobile/tags/page/drive.tag
index 8cc8134bc..23185b14b 100644
--- a/src/web/app/mobile/tags/page/drive.tag
+++ b/src/web/app/mobile/tags/page/drive.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../scripts/ui-event';
 		import Progress from '../../../common/scripts/loading';
 
diff --git a/src/web/app/mobile/tags/page/entrance.tag b/src/web/app/mobile/tags/page/entrance.tag
index b244310cf..17ba1cd7b 100644
--- a/src/web/app/mobile/tags/page/entrance.tag
+++ b/src/web/app/mobile/tags/page/entrance.tag
@@ -42,7 +42,7 @@
 					color rgba(#000, 0.5)
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mode = 'signin';
 
 		this.signup = () => {
diff --git a/src/web/app/mobile/tags/page/home.tag b/src/web/app/mobile/tags/page/home.tag
index 4b9343a10..cf57cdb22 100644
--- a/src/web/app/mobile/tags/page/home.tag
+++ b/src/web/app/mobile/tags/page/home.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../scripts/ui-event';
 		import Progress from '../../../common/scripts/loading';
 		import getPostSummary from '../../../../../common/get-post-summary.ts';
diff --git a/src/web/app/mobile/tags/page/messaging-room.tag b/src/web/app/mobile/tags/page/messaging-room.tag
index 4a1c57b99..67f46e4b1 100644
--- a/src/web/app/mobile/tags/page/messaging-room.tag
+++ b/src/web/app/mobile/tags/page/messaging-room.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../scripts/ui-event';
 
 		this.mixin('api');
diff --git a/src/web/app/mobile/tags/page/messaging.tag b/src/web/app/mobile/tags/page/messaging.tag
index acde6f269..62998c711 100644
--- a/src/web/app/mobile/tags/page/messaging.tag
+++ b/src/web/app/mobile/tags/page/messaging.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../scripts/ui-event';
 
 		this.mixin('page');
diff --git a/src/web/app/mobile/tags/page/notifications.tag b/src/web/app/mobile/tags/page/notifications.tag
index 97717e2e2..eda5a1932 100644
--- a/src/web/app/mobile/tags/page/notifications.tag
+++ b/src/web/app/mobile/tags/page/notifications.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../scripts/ui-event';
 		import Progress from '../../../common/scripts/loading';
 
diff --git a/src/web/app/mobile/tags/page/post.tag b/src/web/app/mobile/tags/page/post.tag
index 296ef140c..5e8cd2448 100644
--- a/src/web/app/mobile/tags/page/post.tag
+++ b/src/web/app/mobile/tags/page/post.tag
@@ -44,7 +44,7 @@
 						margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../scripts/ui-event';
 		import Progress from '../../../common/scripts/loading';
 
diff --git a/src/web/app/mobile/tags/page/search.tag b/src/web/app/mobile/tags/page/search.tag
index 393076367..44af3a2ad 100644
--- a/src/web/app/mobile/tags/page/search.tag
+++ b/src/web/app/mobile/tags/page/search.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../scripts/ui-event';
 		import Progress from '../../../common/scripts/loading';
 
diff --git a/src/web/app/mobile/tags/page/selectdrive.tag b/src/web/app/mobile/tags/page/selectdrive.tag
index ff11bad7d..b410d4603 100644
--- a/src/web/app/mobile/tags/page/selectdrive.tag
+++ b/src/web/app/mobile/tags/page/selectdrive.tag
@@ -52,7 +52,7 @@
 				top 42px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		const q = (new URL(location)).searchParams;
 		this.multiple = q.get('multiple') == 'true' ? true : false;
 
diff --git a/src/web/app/mobile/tags/page/settings.tag b/src/web/app/mobile/tags/page/settings.tag
index beaa08b9a..394c198b0 100644
--- a/src/web/app/mobile/tags/page/settings.tag
+++ b/src/web/app/mobile/tags/page/settings.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../scripts/ui-event';
 
 		this.on('mount', () => {
@@ -91,7 +91,7 @@
 							line-height $height
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import signout from '../../../common/scripts/signout';
 		this.signout = signout;
 
diff --git a/src/web/app/mobile/tags/page/settings/authorized-apps.tag b/src/web/app/mobile/tags/page/settings/authorized-apps.tag
index 0145afc62..35cc961f0 100644
--- a/src/web/app/mobile/tags/page/settings/authorized-apps.tag
+++ b/src/web/app/mobile/tags/page/settings/authorized-apps.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../../scripts/ui-event';
 
 		this.on('mount', () => {
diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
index e213f4070..cafe65f27 100644
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../../scripts/ui-event';
 
 		this.on('mount', () => {
@@ -169,7 +169,7 @@
 						margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 		this.mixin('api');
 
diff --git a/src/web/app/mobile/tags/page/settings/signin.tag b/src/web/app/mobile/tags/page/settings/signin.tag
index 5c9164bcf..7a57406c1 100644
--- a/src/web/app/mobile/tags/page/settings/signin.tag
+++ b/src/web/app/mobile/tags/page/settings/signin.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../../scripts/ui-event';
 
 		this.on('mount', () => {
diff --git a/src/web/app/mobile/tags/page/settings/twitter.tag b/src/web/app/mobile/tags/page/settings/twitter.tag
index 672eff25b..ca5fe2c43 100644
--- a/src/web/app/mobile/tags/page/settings/twitter.tag
+++ b/src/web/app/mobile/tags/page/settings/twitter.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../../scripts/ui-event';
 
 		this.on('mount', () => {
diff --git a/src/web/app/mobile/tags/page/user-followers.tag b/src/web/app/mobile/tags/page/user-followers.tag
index 626c8025d..1123fd422 100644
--- a/src/web/app/mobile/tags/page/user-followers.tag
+++ b/src/web/app/mobile/tags/page/user-followers.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../scripts/ui-event';
 		import Progress from '../../../common/scripts/loading';
 
diff --git a/src/web/app/mobile/tags/page/user-following.tag b/src/web/app/mobile/tags/page/user-following.tag
index 220c5fbf8..b1c22cae1 100644
--- a/src/web/app/mobile/tags/page/user-following.tag
+++ b/src/web/app/mobile/tags/page/user-following.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../scripts/ui-event';
 		import Progress from '../../../common/scripts/loading';
 
diff --git a/src/web/app/mobile/tags/page/user.tag b/src/web/app/mobile/tags/page/user.tag
index 04b727636..3af11bbb4 100644
--- a/src/web/app/mobile/tags/page/user.tag
+++ b/src/web/app/mobile/tags/page/user.tag
@@ -6,7 +6,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../../scripts/ui-event';
 		import Progress from '../../../common/scripts/loading';
 
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index 1c936a8d7..6b70b2313 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -252,7 +252,7 @@
 					border-top 1px solid #eef0f2
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import compile from '../../common/scripts/text-compiler';
 		import getPostSummary from '../../../../common/get-post-summary.ts';
 		import openPostForm from '../scripts/open-post-form';
@@ -444,5 +444,5 @@
 							color #717171
 
 	</style>
-	<script>this.post = this.opts.post</script>
+	<script lang="typescript">this.post = this.opts.post</script>
 </mk-post-detail-sub>
diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
index 01c0748fe..1c0282e77 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -144,7 +144,7 @@
 					box-shadow none
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import Sortable from 'sortablejs';
 		import getKao from '../../common/scripts/get-kao';
 
diff --git a/src/web/app/mobile/tags/post-preview.tag b/src/web/app/mobile/tags/post-preview.tag
index 716916587..3389bf1f0 100644
--- a/src/web/app/mobile/tags/post-preview.tag
+++ b/src/web/app/mobile/tags/post-preview.tag
@@ -90,5 +90,5 @@
 							color #717171
 
 	</style>
-	<script>this.post = this.opts.post</script>
+	<script lang="typescript">this.post = this.opts.post</script>
 </mk-post-preview>
diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag
index 9cb5ee36f..00936a838 100644
--- a/src/web/app/mobile/tags/search-posts.tag
+++ b/src/web/app/mobile/tags/search-posts.tag
@@ -14,7 +14,7 @@
 				margin 16px auto
 				width calc(100% - 32px)
 	</style>
-	<script>
+	<script lang="typescript">
 		import parse from '../../common/scripts/parse-search-query';
 
 		this.mixin('api');
diff --git a/src/web/app/mobile/tags/search.tag b/src/web/app/mobile/tags/search.tag
index ab048ea13..36f375e96 100644
--- a/src/web/app/mobile/tags/search.tag
+++ b/src/web/app/mobile/tags/search.tag
@@ -4,7 +4,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		this.query = this.opts.query;
 
 		this.on('mount', () => {
diff --git a/src/web/app/mobile/tags/sub-post-content.tag b/src/web/app/mobile/tags/sub-post-content.tag
index 27f01fa07..211f59171 100644
--- a/src/web/app/mobile/tags/sub-post-content.tag
+++ b/src/web/app/mobile/tags/sub-post-content.tag
@@ -27,7 +27,7 @@
 				font-size 80%
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import compile from '../../common/scripts/text-compiler';
 
 		this.post = this.opts.post;
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index bf3fa0931..47862a126 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -79,7 +79,7 @@
 						opacity 0.7
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.posts = [];
 		this.init = true;
 		this.fetching = false;
@@ -456,7 +456,7 @@
 									display none
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import compile from '../../common/scripts/text-compiler';
 		import getPostSummary from '../../../../common/get-post-summary.ts';
 		import openPostForm from '../scripts/open-post-form';
@@ -684,5 +684,5 @@
 								font-size 80%
 
 	</style>
-	<script>this.post = this.opts.post</script>
+	<script lang="typescript">this.post = this.opts.post</script>
 </mk-timeline-post-sub>
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index 0c783b8f3..16fb116eb 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -10,7 +10,7 @@
 			display block
 			padding-top 48px
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 
 		this.mixin('stream');
@@ -144,7 +144,7 @@
 						border-left solid 1px rgba(#000, 0.1)
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import ui from '../scripts/ui-event';
 
 		this.mixin('api');
@@ -350,7 +350,7 @@
 					color #777
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 		this.mixin('page');
 		this.mixin('api');
diff --git a/src/web/app/mobile/tags/user-card.tag b/src/web/app/mobile/tags/user-card.tag
index abe46bda0..227b8b389 100644
--- a/src/web/app/mobile/tags/user-card.tag
+++ b/src/web/app/mobile/tags/user-card.tag
@@ -49,7 +49,7 @@
 				margin 8px 0 16px 0
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.user = this.opts.user;
 	</script>
 </mk-user-card>
diff --git a/src/web/app/mobile/tags/user-followers.tag b/src/web/app/mobile/tags/user-followers.tag
index a4dc99e68..02368045e 100644
--- a/src/web/app/mobile/tags/user-followers.tag
+++ b/src/web/app/mobile/tags/user-followers.tag
@@ -5,7 +5,7 @@
 			display block
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.user = this.opts.user;
diff --git a/src/web/app/mobile/tags/user-following.tag b/src/web/app/mobile/tags/user-following.tag
index e1d98297c..c0eb58b4b 100644
--- a/src/web/app/mobile/tags/user-following.tag
+++ b/src/web/app/mobile/tags/user-following.tag
@@ -5,7 +5,7 @@
 			display block
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.user = this.opts.user;
diff --git a/src/web/app/mobile/tags/user-preview.tag b/src/web/app/mobile/tags/user-preview.tag
index 498ad53ec..ec06365e0 100644
--- a/src/web/app/mobile/tags/user-preview.tag
+++ b/src/web/app/mobile/tags/user-preview.tag
@@ -91,5 +91,5 @@
 						color #717171
 
 	</style>
-	<script>this.user = this.opts.user</script>
+	<script lang="typescript">this.user = this.opts.user</script>
 </mk-user-preview>
diff --git a/src/web/app/mobile/tags/user-timeline.tag b/src/web/app/mobile/tags/user-timeline.tag
index dd878810c..270a3744c 100644
--- a/src/web/app/mobile/tags/user-timeline.tag
+++ b/src/web/app/mobile/tags/user-timeline.tag
@@ -6,7 +6,7 @@
 			max-width 600px
 			margin 0 auto
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.user = this.opts.user;
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index 316fb764e..d0874f8e7 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -185,7 +185,7 @@
 						padding 16px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.age = require('s-age');
 
 		this.mixin('i');
@@ -299,7 +299,7 @@
 				color #cad2da
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 
 		this.user = this.opts.user;
@@ -341,7 +341,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.user = this.opts.user;
@@ -427,7 +427,7 @@
 					color #aaa
 
 	</style>
-	<script>
+	<script lang="typescript">
 		import summary from '../../../../common/get-post-summary.ts';
 
 		this.post = this.opts.post;
@@ -477,7 +477,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.images = [];
@@ -534,7 +534,7 @@
 					transform-origin center
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.user = this.opts.user;
@@ -586,7 +586,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.user = this.opts.user;
 	</script>
 </mk-user-overview-keywords>
@@ -620,7 +620,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.user = this.opts.user;
 	</script>
 </mk-user-overview-domains>
@@ -658,7 +658,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.user = this.opts.user;
@@ -713,7 +713,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.user = this.opts.user;
diff --git a/src/web/app/mobile/tags/users-list.tag b/src/web/app/mobile/tags/users-list.tag
index 17b69e9e1..fb7040a7a 100644
--- a/src/web/app/mobile/tags/users-list.tag
+++ b/src/web/app/mobile/tags/users-list.tag
@@ -77,7 +77,7 @@
 					margin-right 4px
 
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('i');
 
 		this.limit = 30;
diff --git a/src/web/app/stats/tags/index.tag b/src/web/app/stats/tags/index.tag
index 84866c3d1..3b2b10b0a 100644
--- a/src/web/app/stats/tags/index.tag
+++ b/src/web/app/stats/tags/index.tag
@@ -40,7 +40,7 @@
 				> a
 					color #546567
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.initializing = true;
@@ -63,7 +63,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.initializing = true;
@@ -89,7 +89,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		this.mixin('api');
 
 		this.initializing = true;
@@ -142,7 +142,7 @@
 				padding 1px
 				width 100%
 	</style>
-	<script>
+	<script lang="typescript">
 		this.viewBoxX = 365;
 		this.viewBoxY = 80;
 
@@ -187,7 +187,7 @@
 				padding 1px
 				width 100%
 	</style>
-	<script>
+	<script lang="typescript">
 		this.viewBoxX = 365;
 		this.viewBoxY = 80;
 
diff --git a/src/web/app/status/tags/index.tag b/src/web/app/status/tags/index.tag
index 9ac54c867..e06258c49 100644
--- a/src/web/app/status/tags/index.tag
+++ b/src/web/app/status/tags/index.tag
@@ -50,7 +50,7 @@
 				> a
 					color #546567
 	</style>
-	<script>
+	<script lang="typescript">
 		import Connection from '../../common/scripts/streaming/server-stream';
 
 		this.mixin('api');
@@ -81,7 +81,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		this.connection = this.opts.connection;
 
 		this.on('mount', () => {
@@ -111,7 +111,7 @@
 		:scope
 			display block
 	</style>
-	<script>
+	<script lang="typescript">
 		this.connection = this.opts.connection;
 
 		this.on('mount', () => {
@@ -176,7 +176,7 @@
 				padding 1px
 				width 100%
 	</style>
-	<script>
+	<script lang="typescript">
 		import uuid from 'uuid';
 
 		this.viewBoxX = 100;

From 359855f2cce134fc39c00d6ae3a93917350f29e6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 7 Feb 2018 19:00:55 +0900
Subject: [PATCH 011/286] wip

---
 src/web/app/common/tags/reaction-icon.tag | 21 ---------------------
 src/web/app/common/tags/reaction-icon.vue | 20 ++++++++++++++++++++
 2 files changed, 20 insertions(+), 21 deletions(-)
 delete mode 100644 src/web/app/common/tags/reaction-icon.tag
 create mode 100644 src/web/app/common/tags/reaction-icon.vue

diff --git a/src/web/app/common/tags/reaction-icon.tag b/src/web/app/common/tags/reaction-icon.tag
deleted file mode 100644
index 2282a5868..000000000
--- a/src/web/app/common/tags/reaction-icon.tag
+++ /dev/null
@@ -1,21 +0,0 @@
-<mk-reaction-icon>
-	<virtual v-if="opts.reaction == 'like'"><img src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%"></virtual>
-	<virtual v-if="opts.reaction == 'love'"><img src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%"></virtual>
-	<virtual v-if="opts.reaction == 'laugh'"><img src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%"></virtual>
-	<virtual v-if="opts.reaction == 'hmm'"><img src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%"></virtual>
-	<virtual v-if="opts.reaction == 'surprise'"><img src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%"></virtual>
-	<virtual v-if="opts.reaction == 'congrats'"><img src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%"></virtual>
-	<virtual v-if="opts.reaction == 'angry'"><img src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%"></virtual>
-	<virtual v-if="opts.reaction == 'confused'"><img src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%"></virtual>
-	<virtual v-if="opts.reaction == 'pudding'"><img src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%"></virtual>
-
-	<style lang="stylus" scoped>
-		:scope
-			display inline
-
-			img
-				vertical-align middle
-				width 1em
-				height 1em
-	</style>
-</mk-reaction-icon>
diff --git a/src/web/app/common/tags/reaction-icon.vue b/src/web/app/common/tags/reaction-icon.vue
new file mode 100644
index 000000000..317daf0fe
--- /dev/null
+++ b/src/web/app/common/tags/reaction-icon.vue
@@ -0,0 +1,20 @@
+<template>
+<span>
+	<img v-if="reaction == 'like'" src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%">
+	<img v-if="reaction == 'love'" src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%">
+	<img v-if="reaction == 'laugh'" src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%">
+	<img v-if="reaction == 'hmm'" src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%">
+	<img v-if="reaction == 'surprise'" src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%">
+	<img v-if="reaction == 'congrats'" src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%">
+	<img v-if="reaction == 'angry'" src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%">
+	<img v-if="reaction == 'confused'" src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%">
+	<img v-if="reaction == 'pudding'" src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%">
+</span>
+</template>
+
+<style lang="stylus" scoped>
+	img
+		vertical-align middle
+		width 1em
+		height 1em
+</style>

From 50a6bcb693806bacaac0ad339ca3dc46de350f65 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Feb 2018 14:02:08 +0900
Subject: [PATCH 012/286] wip

---
 src/web/app/ch/tags/channel.tag               |  2 +-
 .../app/common/tags/{poll.tag => poll.vue}    | 88 ++++++++++---------
 src/web/app/common/tags/signin.tag            |  2 +-
 .../app/desktop/tags/big-follow-button.tag    |  2 +-
 src/web/app/desktop/tags/drive/browser.tag    |  2 +-
 src/web/app/desktop/tags/follow-button.tag    |  2 +-
 src/web/app/desktop/tags/post-detail.tag      |  2 +-
 src/web/app/desktop/tags/post-form.tag        |  4 +-
 src/web/app/desktop/tags/settings.tag         | 20 ++---
 src/web/app/desktop/tags/timeline.tag         |  2 +-
 src/web/app/mobile/tags/follow-button.tag     |  2 +-
 .../app/mobile/tags/notification-preview.tag  |  2 +-
 src/web/app/mobile/tags/notification.tag      |  2 +-
 src/web/app/mobile/tags/post-detail.tag       |  2 +-
 src/web/app/mobile/tags/timeline.tag          |  4 +-
 15 files changed, 73 insertions(+), 65 deletions(-)
 rename src/web/app/common/tags/{poll.tag => poll.vue} (58%)

diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index a706a247f..d71837af4 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -246,7 +246,7 @@
 	<div class="actions">
 		<button @click="selectFile">%fa:upload%%i18n:ch.tags.mk-channel-form.upload%</button>
 		<button @click="drive">%fa:cloud%%i18n:ch.tags.mk-channel-form.drive%</button>
-		<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0) } @click="post">
+		<button :class="{ wait: wait }" ref="submit" disabled={ wait || (refs.text.value.length == 0) } @click="post">
 			<virtual v-if="!wait">%fa:paper-plane%</virtual>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.post%' }<mk-ellipsis v-if="wait"/>
 		</button>
 	</div>
diff --git a/src/web/app/common/tags/poll.tag b/src/web/app/common/tags/poll.vue
similarity index 58%
rename from src/web/app/common/tags/poll.tag
rename to src/web/app/common/tags/poll.vue
index c0605d890..638fa1cbe 100644
--- a/src/web/app/common/tags/poll.tag
+++ b/src/web/app/common/tags/poll.vue
@@ -1,6 +1,7 @@
-<mk-poll data-is-voted={ isVoted }>
+<template>
+<div :data-is-voted="isVoted">
 	<ul>
-		<li each={ poll.choices } @click="vote.bind(null, id)" class={ voted: voted } title={ !parent.isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', text) : '' }>
+		<li v-for="choice in poll.choices" @click="vote.bind(choice.id)" :class="{ voted: choice.voted }" title={ !parent.isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', text) : '' }>
 			<div class="backdrop" style={ 'width:' + (parent.result ? (votes / parent.total * 100) : 0) + '%' }></div>
 			<span>
 				<virtual v-if="is_voted">%fa:check%</virtual>
@@ -15,6 +16,51 @@
 		<a v-if="!isVoted" @click="toggleResult">{ result ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }</a>
 		<span v-if="isVoted">%i18n:common.tags.mk-poll.voted%</span>
 	</p>
+</div>
+</template>
+
+<script lang="typescript">
+	this.mixin('api');
+
+	this.init = post => {
+		this.post = post;
+		this.poll = this.post.poll;
+		this.total = this.poll.choices.reduce((a, b) => a + b.votes, 0);
+		this.isVoted = this.poll.choices.some(c => c.is_voted);
+		this.result = this.isVoted;
+		this.update();
+	};
+
+	this.init(this.opts.post);
+
+	this.toggleResult = () => {
+		this.result = !this.result;
+	};
+
+	this.vote = id => {
+		if (this.poll.choices.some(c => c.is_voted)) return;
+		this.api('posts/polls/vote', {
+			post_id: this.post.id,
+			choice: id
+		}).then(() => {
+			this.poll.choices.forEach(c => {
+				if (c.id == id) {
+					c.votes++;
+					c.is_voted = true;
+				}
+			});
+			this.update({
+				poll: this.poll,
+				isVoted: true,
+				result: true,
+				total: this.total + 1
+			});
+		});
+	};
+</script>
+
+<mk-poll data-is-voted={ isVoted }>
+
 	<style lang="stylus" scoped>
 		:scope
 			display block
@@ -67,43 +113,5 @@
 						background transparent
 
 	</style>
-	<script lang="typescript">
-		this.mixin('api');
 
-		this.init = post => {
-			this.post = post;
-			this.poll = this.post.poll;
-			this.total = this.poll.choices.reduce((a, b) => a + b.votes, 0);
-			this.isVoted = this.poll.choices.some(c => c.is_voted);
-			this.result = this.isVoted;
-			this.update();
-		};
-
-		this.init(this.opts.post);
-
-		this.toggleResult = () => {
-			this.result = !this.result;
-		};
-
-		this.vote = id => {
-			if (this.poll.choices.some(c => c.is_voted)) return;
-			this.api('posts/polls/vote', {
-				post_id: this.post.id,
-				choice: id
-			}).then(() => {
-				this.poll.choices.forEach(c => {
-					if (c.id == id) {
-						c.votes++;
-						c.is_voted = true;
-					}
-				});
-				this.update({
-					poll: this.poll,
-					isVoted: true,
-					result: true,
-					total: this.total + 1
-				});
-			});
-		};
-	</script>
 </mk-poll>
diff --git a/src/web/app/common/tags/signin.tag b/src/web/app/common/tags/signin.tag
index 441a8ec56..76a55c7e0 100644
--- a/src/web/app/common/tags/signin.tag
+++ b/src/web/app/common/tags/signin.tag
@@ -1,5 +1,5 @@
 <mk-signin>
-	<form class={ signing: signing } onsubmit={ onsubmit }>
+	<form :class="{ signing: signing }" onsubmit={ onsubmit }>
 		<label class="user-name">
 			<input ref="username" type="text" pattern="^[a-zA-Z0-9-]+$" placeholder="%i18n:common.tags.mk-signin.username%" autofocus="autofocus" required="required" oninput={ oninput }/>%fa:at%
 		</label>
diff --git a/src/web/app/desktop/tags/big-follow-button.tag b/src/web/app/desktop/tags/big-follow-button.tag
index 6d43e4abe..09b587c37 100644
--- a/src/web/app/desktop/tags/big-follow-button.tag
+++ b/src/web/app/desktop/tags/big-follow-button.tag
@@ -1,5 +1,5 @@
 <mk-big-follow-button>
-	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } v-if="!init" @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
+	<button :class="{ wait: wait, follow: !user.is_following, unfollow: user.is_following }" v-if="!init" @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
 		<span v-if="!wait && user.is_following">%fa:minus%フォロー解除</span>
 		<span v-if="!wait && !user.is_following">%fa:plus%フォロー</span>
 		<virtual v-if="wait">%fa:spinner .pulse .fw%</virtual>
diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/tags/drive/browser.tag
index 15c9bb569..9fdb27054 100644
--- a/src/web/app/desktop/tags/drive/browser.tag
+++ b/src/web/app/desktop/tags/drive/browser.tag
@@ -1,7 +1,7 @@
 <mk-drive-browser>
 	<nav>
 		<div class="path" oncontextmenu={ pathOncontextmenu }>
-			<mk-drive-browser-nav-folder class={ current: folder == null } folder={ null }/>
+			<mk-drive-browser-nav-folder :class="{ current: folder == null }" folder={ null }/>
 			<virtual each={ folder in hierarchyFolders }>
 				<span class="separator">%fa:angle-right%</span>
 				<mk-drive-browser-nav-folder folder={ folder }/>
diff --git a/src/web/app/desktop/tags/follow-button.tag b/src/web/app/desktop/tags/follow-button.tag
index 843774ad0..9a01b0831 100644
--- a/src/web/app/desktop/tags/follow-button.tag
+++ b/src/web/app/desktop/tags/follow-button.tag
@@ -1,5 +1,5 @@
 <mk-follow-button>
-	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } v-if="!init" @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
+	<button :class="{ wait: wait, follow: !user.is_following, unfollow: user.is_following }" v-if="!init" @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
 		<virtual v-if="!wait && user.is_following">%fa:minus%</virtual>
 		<virtual v-if="!wait && !user.is_following">%fa:plus%</virtual>
 		<virtual v-if="wait">%fa:spinner .pulse .fw%</virtual>
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index 34b34b6a5..2225733f7 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -49,7 +49,7 @@
 				<button @click="repost" title="Repost">
 					%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
 				</button>
-				<button class={ reacted: p.my_reaction != null } @click="react" ref="reactButton" title="リアクション">
+				<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton" title="リアクション">
 					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
 				</button>
 				<button @click="menu" ref="menuButton">
diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag
index c2da85885..358deb82f 100644
--- a/src/web/app/desktop/tags/post-form.tag
+++ b/src/web/app/desktop/tags/post-form.tag
@@ -1,6 +1,6 @@
 <mk-post-form ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop }>
 	<div class="content">
-		<textarea class={ with: (files.length != 0 || poll) } ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ placeholder }></textarea>
+		<textarea :class="{ with: (files.length != 0 || poll) }" ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ placeholder }></textarea>
 		<div class="medias { with: poll }" show={ files.length != 0 }>
 			<ul ref="media">
 				<li each={ files } data-id={ id }>
@@ -18,7 +18,7 @@
 	<button class="kao" title="%i18n:desktop.tags.mk-post-form.insert-a-kao%" @click="kao">%fa:R smile%</button>
 	<button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" @click="addPoll">%fa:chart-pie%</button>
 	<p class="text-count { over: refs.text.value.length > 1000 }">{ '%i18n:desktop.tags.mk-post-form.text-remain%'.replace('{}', 1000 - refs.text.value.length) }</p>
-	<button class={ wait: wait } ref="submit" disabled={ wait || (refs.text.value.length == 0 && files.length == 0 && !poll && !repost) } @click="post">
+	<button :class="{ wait: wait }" ref="submit" disabled={ wait || (refs.text.value.length == 0 && files.length == 0 && !poll && !repost) } @click="post">
 		{ wait ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }<mk-ellipsis v-if="wait"/>
 	</button>
 	<input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" onchange={ changeFile }/>
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 3288ba721..191d1d754 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -1,15 +1,15 @@
 <mk-settings>
 	<div class="nav">
-		<p class={ active: page == 'profile' } onmousedown={ setPage.bind(null, 'profile') }>%fa:user .fw%%i18n:desktop.tags.mk-settings.profile%</p>
-		<p class={ active: page == 'web' } onmousedown={ setPage.bind(null, 'web') }>%fa:desktop .fw%Web</p>
-		<p class={ active: page == 'notification' } onmousedown={ setPage.bind(null, 'notification') }>%fa:R bell .fw%通知</p>
-		<p class={ active: page == 'drive' } onmousedown={ setPage.bind(null, 'drive') }>%fa:cloud .fw%%i18n:desktop.tags.mk-settings.drive%</p>
-		<p class={ active: page == 'mute' } onmousedown={ setPage.bind(null, 'mute') }>%fa:ban .fw%%i18n:desktop.tags.mk-settings.mute%</p>
-		<p class={ active: page == 'apps' } onmousedown={ setPage.bind(null, 'apps') }>%fa:puzzle-piece .fw%アプリ</p>
-		<p class={ active: page == 'twitter' } onmousedown={ setPage.bind(null, 'twitter') }>%fa:B twitter .fw%Twitter</p>
-		<p class={ active: page == 'security' } onmousedown={ setPage.bind(null, 'security') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p>
-		<p class={ active: page == 'api' } onmousedown={ setPage.bind(null, 'api') }>%fa:key .fw%API</p>
-		<p class={ active: page == 'other' } onmousedown={ setPage.bind(null, 'other') }>%fa:cogs .fw%%i18n:desktop.tags.mk-settings.other%</p>
+		<p :class="{ active: page == 'profile' }" onmousedown={ setPage.bind(null, 'profile') }>%fa:user .fw%%i18n:desktop.tags.mk-settings.profile%</p>
+		<p :class="{ active: page == 'web' }" onmousedown={ setPage.bind(null, 'web') }>%fa:desktop .fw%Web</p>
+		<p :class="{ active: page == 'notification' }" onmousedown={ setPage.bind(null, 'notification') }>%fa:R bell .fw%通知</p>
+		<p :class="{ active: page == 'drive' }" onmousedown={ setPage.bind(null, 'drive') }>%fa:cloud .fw%%i18n:desktop.tags.mk-settings.drive%</p>
+		<p :class="{ active: page == 'mute' }" onmousedown={ setPage.bind(null, 'mute') }>%fa:ban .fw%%i18n:desktop.tags.mk-settings.mute%</p>
+		<p :class="{ active: page == 'apps' }" onmousedown={ setPage.bind(null, 'apps') }>%fa:puzzle-piece .fw%アプリ</p>
+		<p :class="{ active: page == 'twitter' }" onmousedown={ setPage.bind(null, 'twitter') }>%fa:B twitter .fw%Twitter</p>
+		<p :class="{ active: page == 'security' }" onmousedown={ setPage.bind(null, 'security') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p>
+		<p :class="{ active: page == 'api' }" onmousedown={ setPage.bind(null, 'api') }>%fa:key .fw%API</p>
+		<p :class="{ active: page == 'other' }" onmousedown={ setPage.bind(null, 'other') }>%fa:cogs .fw%%i18n:desktop.tags.mk-settings.other%</p>
 	</div>
 	<div class="pages">
 		<section class="profile" show={ page == 'profile' }>
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 485353346..772140dcc 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -135,7 +135,7 @@
 				<button @click="repost" title="%i18n:desktop.tags.mk-timeline-post.repost%">
 					%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
 				</button>
-				<button class={ reacted: p.my_reaction != null } @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-post.add-reaction%">
+				<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-post.add-reaction%">
 					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
 				</button>
 				<button @click="menu" ref="menuButton">
diff --git a/src/web/app/mobile/tags/follow-button.tag b/src/web/app/mobile/tags/follow-button.tag
index bd4ecbaf9..5f746c46b 100644
--- a/src/web/app/mobile/tags/follow-button.tag
+++ b/src/web/app/mobile/tags/follow-button.tag
@@ -1,5 +1,5 @@
 <mk-follow-button>
-	<button class={ wait: wait, follow: !user.is_following, unfollow: user.is_following } v-if="!init" @click="onclick" disabled={ wait }>
+	<button :class="{ wait: wait, follow: !user.is_following, unfollow: user.is_following }" v-if="!init" @click="onclick" disabled={ wait }>
 		<virtual v-if="!wait && user.is_following">%fa:minus%</virtual>
 		<virtual v-if="!wait && !user.is_following">%fa:plus%</virtual>
 		<virtual v-if="wait">%fa:spinner .pulse .fw%</virtual>{ user.is_following ? '%i18n:mobile.tags.mk-follow-button.unfollow%' : '%i18n:mobile.tags.mk-follow-button.follow%' }
diff --git a/src/web/app/mobile/tags/notification-preview.tag b/src/web/app/mobile/tags/notification-preview.tag
index 06f4fb511..bd4f633f8 100644
--- a/src/web/app/mobile/tags/notification-preview.tag
+++ b/src/web/app/mobile/tags/notification-preview.tag
@@ -1,4 +1,4 @@
-<mk-notification-preview class={ notification.type }>
+<mk-notification-preview :class="{ notification.type }">
 	<virtual v-if="notification.type == 'reaction'">
 		<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
diff --git a/src/web/app/mobile/tags/notification.tag b/src/web/app/mobile/tags/notification.tag
index 9aca50cb4..d4f6ca92e 100644
--- a/src/web/app/mobile/tags/notification.tag
+++ b/src/web/app/mobile/tags/notification.tag
@@ -1,4 +1,4 @@
-<mk-notification class={ notification.type }>
+<mk-notification :class="{ notification.type }">
 	<mk-time time={ notification.created_at }/>
 	<virtual v-if="notification.type == 'reaction'">
 		<a class="avatar-anchor" href={ '/' + notification.user.username }>
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index 6b70b2313..124a707d2 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -49,7 +49,7 @@
 			<button @click="repost" title="Repost">
 				%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
 			</button>
-			<button class={ reacted: p.my_reaction != null } @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-post-detail.reaction%">
+			<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-post-detail.reaction%">
 				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
 			</button>
 			<button @click="menu" ref="menuButton">
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 47862a126..b1ff03547 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -136,7 +136,7 @@
 	</script>
 </mk-timeline>
 
-<mk-timeline-post class={ repost: isRepost }>
+<mk-timeline-post :class="{ repost: isRepost }">
 	<div class="reply-to" v-if="p.reply">
 		<mk-timeline-post-sub post={ p.reply }/>
 	</div>
@@ -188,7 +188,7 @@
 				<button @click="repost" title="Repost">
 					%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
 				</button>
-				<button class={ reacted: p.my_reaction != null } @click="react" ref="reactButton">
+				<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton">
 					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
 				</button>
 				<button class="menu" @click="menu" ref="menuButton">

From 322da7560c2aeccdcbb9503f1ae48eecef6fbb21 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Feb 2018 14:03:43 +0900
Subject: [PATCH 013/286] wip

---
 src/web/app/common/tags/reaction-picker.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/common/tags/reaction-picker.vue b/src/web/app/common/tags/reaction-picker.vue
index 8f0f8956e..28e151ce7 100644
--- a/src/web/app/common/tags/reaction-picker.vue
+++ b/src/web/app/common/tags/reaction-picker.vue
@@ -1,7 +1,7 @@
 <template>
 <div>
 	<div class="backdrop" ref="backdrop" @click="close"></div>
-	<div class="popover" :data-compact="compact" ref="popover">
+	<div class="popover" :class="{ compact }" ref="popover">
 		<p v-if="!compact">{{ title }}</p>
 		<div>
 			<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>

From f8c85590896c4b8d8d8eaca3e518288585d129b6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Feb 2018 14:25:49 +0900
Subject: [PATCH 014/286] wip

---
 src/web/app/common/tags/reaction-picker.vue  | 22 ++++++----
 src/web/app/common/tags/reactions-viewer.vue |  2 +-
 src/web/app/common/tags/stream-indicator.vue |  2 +-
 src/web/app/common/tags/time.vue             | 42 +++++++++++---------
 src/web/app/common/tags/url-preview.vue      | 20 +++++-----
 src/web/app/common/tags/url.vue              | 20 +++++-----
 6 files changed, 61 insertions(+), 47 deletions(-)

diff --git a/src/web/app/common/tags/reaction-picker.vue b/src/web/app/common/tags/reaction-picker.vue
index 28e151ce7..dd4d1380b 100644
--- a/src/web/app/common/tags/reaction-picker.vue
+++ b/src/web/app/common/tags/reaction-picker.vue
@@ -21,15 +21,21 @@
 <script lang="typescript">
 	import anime from 'animejs';
 	import api from '../scripts/api';
+	import MkReactionIcon from './reaction-icon.vue';
 
 	const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%';
 
 	export default {
-		props: ['post', 'source', 'compact', 'cb'],
-		data: {
-			title: placeholder
+		components: {
+			MkReactionIcon
 		},
-		created: function() {
+		props: ['post', 'source', 'compact', 'cb'],
+		data() {
+			return {
+				title: placeholder
+			};
+		},
+		created() {
 			const rect = this.source.getBoundingClientRect();
 			const width = this.$refs.popover.offsetWidth;
 			const height = this.$refs.popover.offsetHeight;
@@ -60,7 +66,7 @@
 			});
 		},
 		methods: {
-			react: function(reaction) {
+			react(reaction) {
 				api('posts/reactions/create', {
 					post_id: this.post.id,
 					reaction: reaction
@@ -69,13 +75,13 @@
 					this.$destroy();
 				});
 			},
-			onMouseover: function(e) {
+			onMouseover(e) {
 				this.title = e.target.title;
 			},
-			onMouseout: function(e) {
+			onMouseout(e) {
 				this.title = placeholder;
 			},
-			clo1se: function() {
+			close() {
 				this.$refs.backdrop.style.pointerEvents = 'none';
 				anime({
 					targets: this.$refs.backdrop,
diff --git a/src/web/app/common/tags/reactions-viewer.vue b/src/web/app/common/tags/reactions-viewer.vue
index 32fa50801..f6e37caa4 100644
--- a/src/web/app/common/tags/reactions-viewer.vue
+++ b/src/web/app/common/tags/reactions-viewer.vue
@@ -18,7 +18,7 @@
 	export default {
 		props: ['post'],
 		computed: {
-			reactions: function() {
+			reactions() {
 				return this.post.reaction_counts;
 			}
 		}
diff --git a/src/web/app/common/tags/stream-indicator.vue b/src/web/app/common/tags/stream-indicator.vue
index ea8fa5adf..0721c77ad 100644
--- a/src/web/app/common/tags/stream-indicator.vue
+++ b/src/web/app/common/tags/stream-indicator.vue
@@ -21,7 +21,7 @@
 
 	export default {
 		props: ['stream'],
-		created: function() {
+		created() {
 			if (this.stream.state == 'connected') {
 				this.root.style.opacity = 0;
 			}
diff --git a/src/web/app/common/tags/time.vue b/src/web/app/common/tags/time.vue
index 82d8ecbfd..0239f5422 100644
--- a/src/web/app/common/tags/time.vue
+++ b/src/web/app/common/tags/time.vue
@@ -9,11 +9,13 @@
 <script lang="typescript">
 	export default {
 		props: ['time', 'mode'],
-		data: {
-			mode: 'relative',
-			tickId: null,
+		data() {
+			return {
+				mode: 'relative',
+				tickId: null
+			};
 		},
-		created: function() {
+		created() {
 			this.absolute =
 				this.time.getFullYear()    + '年' +
 				(this.time.getMonth() + 1) + '月' +
@@ -27,25 +29,27 @@
 				this.tickId = setInterval(this.tick, 1000);
 			}
 		},
-		destroyed: function() {
+		destroyed() {
 			if (this.mode === 'relative' || this.mode === 'detail') {
 				clearInterval(this.tickId);
 			}
 		},
-		tick: function() {
-			const now = new Date();
-			const ago = (now - this.time) / 1000/*ms*/;
-			this.relative =
-				ago >= 31536000 ? '%i18n:common.time.years_ago%'  .replace('{}', ~~(ago / 31536000)) :
-				ago >= 2592000  ? '%i18n:common.time.months_ago%' .replace('{}', ~~(ago / 2592000)) :
-				ago >= 604800   ? '%i18n:common.time.weeks_ago%'  .replace('{}', ~~(ago / 604800)) :
-				ago >= 86400    ? '%i18n:common.time.days_ago%'   .replace('{}', ~~(ago / 86400)) :
-				ago >= 3600     ? '%i18n:common.time.hours_ago%'  .replace('{}', ~~(ago / 3600)) :
-				ago >= 60       ? '%i18n:common.time.minutes_ago%'.replace('{}', ~~(ago / 60)) :
-				ago >= 10       ? '%i18n:common.time.seconds_ago%'.replace('{}', ~~(ago % 60)) :
-				ago >= 0        ? '%i18n:common.time.just_now%' :
-				ago <  0        ? '%i18n:common.time.future%' :
-				'%i18n:common.time.unknown%';
+		methods: {
+			tick() {
+				const now = new Date();
+				const ago = (now - this.time) / 1000/*ms*/;
+				this.relative =
+					ago >= 31536000 ? '%i18n:common.time.years_ago%'  .replace('{}', ~~(ago / 31536000)) :
+					ago >= 2592000  ? '%i18n:common.time.months_ago%' .replace('{}', ~~(ago / 2592000)) :
+					ago >= 604800   ? '%i18n:common.time.weeks_ago%'  .replace('{}', ~~(ago / 604800)) :
+					ago >= 86400    ? '%i18n:common.time.days_ago%'   .replace('{}', ~~(ago / 86400)) :
+					ago >= 3600     ? '%i18n:common.time.hours_ago%'  .replace('{}', ~~(ago / 3600)) :
+					ago >= 60       ? '%i18n:common.time.minutes_ago%'.replace('{}', ~~(ago / 60)) :
+					ago >= 10       ? '%i18n:common.time.seconds_ago%'.replace('{}', ~~(ago % 60)) :
+					ago >= 0        ? '%i18n:common.time.just_now%' :
+					ago <  0        ? '%i18n:common.time.future%' :
+					'%i18n:common.time.unknown%';
+			}
 		}
 	};
 </script>
diff --git a/src/web/app/common/tags/url-preview.vue b/src/web/app/common/tags/url-preview.vue
index 45a718d3e..88158db84 100644
--- a/src/web/app/common/tags/url-preview.vue
+++ b/src/web/app/common/tags/url-preview.vue
@@ -17,7 +17,17 @@
 <script lang="typescript">
 	export default {
 		props: ['url'],
-		created: function() {
+		data() {
+			return {
+				fetching: true,
+				title: null,
+				description: null,
+				thumbnail: null,
+				icon: null,
+				sitename: null
+			};
+		},
+		created() {
 			fetch('/api:url?url=' + this.url).then(res => {
 				res.json().then(info => {
 					this.title = info.title;
@@ -29,14 +39,6 @@
 					this.fetching = false;
 				});
 			});
-		},
-		data: {
-			fetching: true,
-			title: null,
-			description: null,
-			thumbnail: null,
-			icon: null,
-			sitename: null
 		}
 	};
 </script>
diff --git a/src/web/app/common/tags/url.vue b/src/web/app/common/tags/url.vue
index fdc8a1cb2..4cc76f7e2 100644
--- a/src/web/app/common/tags/url.vue
+++ b/src/web/app/common/tags/url.vue
@@ -13,7 +13,17 @@
 <script lang="typescript">
 	export default {
 		props: ['url', 'target'],
-		created: function() {
+		data() {
+			return {
+				schema: null,
+				hostname: null,
+				port: null,
+				pathname: null,
+				query: null,
+				hash: null
+			};
+		},
+		created() {
 			const url = new URL(this.url);
 
 			this.schema = url.protocol;
@@ -22,14 +32,6 @@
 			this.pathname = url.pathname;
 			this.query = url.search;
 			this.hash = url.hash;
-		},
-		data: {
-			schema: null,
-			hostname: null,
-			port: null,
-			pathname: null,
-			query: null,
-			hash: null
 		}
 	};
 </script>

From 43525c4839e2bea8818cf2fcd236a1f71d5b7473 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Feb 2018 14:50:18 +0900
Subject: [PATCH 015/286] wip

---
 src/web/app/auth/tags/form.tag                |  4 +--
 src/web/app/ch/tags/channel.tag               | 10 +++---
 src/web/app/common/tags/error.tag             |  6 ++--
 src/web/app/common/tags/file-type-icon.tag    |  2 +-
 src/web/app/common/tags/messaging/form.tag    |  2 +-
 src/web/app/common/tags/messaging/index.tag   |  4 +--
 src/web/app/common/tags/messaging/message.tag |  2 +-
 src/web/app/common/tags/messaging/room.tag    |  6 ++--
 src/web/app/common/tags/poll.vue              | 10 +++---
 src/web/app/common/tags/signin-history.tag    |  4 +--
 src/web/app/common/tags/signup.tag            |  2 +-
 .../app/desktop/tags/big-follow-button.tag    |  2 +-
 src/web/app/desktop/tags/dialog.tag           |  4 +--
 src/web/app/desktop/tags/drive/browser.tag    | 12 +++----
 src/web/app/desktop/tags/drive/folder.tag     |  2 +-
 src/web/app/desktop/tags/drive/nav-folder.tag |  2 +-
 src/web/app/desktop/tags/follow-button.tag    |  6 ++--
 .../desktop/tags/home-widgets/access-log.tag  |  4 +--
 .../desktop/tags/home-widgets/broadcast.tag   |  2 +-
 .../app/desktop/tags/home-widgets/channel.tag |  8 ++---
 .../desktop/tags/home-widgets/mentions.tag    |  4 +--
 .../desktop/tags/home-widgets/messaging.tag   |  4 +--
 .../tags/home-widgets/notifications.tag       |  4 +--
 .../tags/home-widgets/photo-stream.tag        |  8 ++---
 .../desktop/tags/home-widgets/post-form.tag   |  8 ++---
 .../tags/home-widgets/recommended-polls.tag   |  4 +--
 .../desktop/tags/home-widgets/rss-reader.tag  |  6 ++--
 .../app/desktop/tags/home-widgets/server.tag  |  4 +--
 .../desktop/tags/home-widgets/timeline.tag    |  4 +--
 .../app/desktop/tags/home-widgets/trends.tag  |  4 +--
 .../tags/home-widgets/user-recommendation.tag |  4 +--
 src/web/app/desktop/tags/images.tag           |  4 +--
 src/web/app/desktop/tags/notifications.tag    | 34 +++++++++----------
 src/web/app/desktop/tags/post-detail.tag      | 12 +++----
 src/web/app/desktop/tags/repost-form.tag      |  8 ++---
 src/web/app/desktop/tags/search-posts.tag     |  4 +--
 src/web/app/desktop/tags/settings.tag         |  4 +--
 src/web/app/desktop/tags/timeline.tag         |  8 ++---
 src/web/app/desktop/tags/ui.tag               | 10 +++---
 src/web/app/desktop/tags/user-preview.tag     |  4 +--
 src/web/app/desktop/tags/user-timeline.tag    |  4 +--
 src/web/app/desktop/tags/user.tag             |  8 ++---
 src/web/app/desktop/tags/widgets/activity.tag |  4 +--
 src/web/app/desktop/tags/widgets/calendar.tag |  4 +--
 src/web/app/dev/tags/pages/apps.tag           |  4 +--
 src/web/app/mobile/tags/drive.tag             | 26 +++++++-------
 src/web/app/mobile/tags/drive/file-viewer.tag |  2 +-
 src/web/app/mobile/tags/follow-button.tag     |  6 ++--
 src/web/app/mobile/tags/images.tag            |  4 +--
 src/web/app/mobile/tags/init-following.tag    |  4 +--
 .../app/mobile/tags/notification-preview.tag  | 28 +++++++--------
 src/web/app/mobile/tags/notification.tag      | 28 +++++++--------
 src/web/app/mobile/tags/notifications.tag     |  6 ++--
 src/web/app/mobile/tags/post-detail.tag       | 12 +++----
 src/web/app/mobile/tags/timeline.tag          |  4 +--
 src/web/app/mobile/tags/ui.tag                |  6 ++--
 src/web/app/mobile/tags/user.tag              | 24 ++++++-------
 57 files changed, 205 insertions(+), 205 deletions(-)

diff --git a/src/web/app/auth/tags/form.tag b/src/web/app/auth/tags/form.tag
index 9b317fef4..f20165977 100644
--- a/src/web/app/auth/tags/form.tag
+++ b/src/web/app/auth/tags/form.tag
@@ -11,7 +11,7 @@
 		<section>
 			<h2>このアプリは次の権限を要求しています:</h2>
 			<ul>
-				<virtual each={ p in app.permission }>
+				<template each={ p in app.permission }>
 					<li v-if="p == 'account-read'">アカウントの情報を見る。</li>
 					<li v-if="p == 'account-write'">アカウントの情報を操作する。</li>
 					<li v-if="p == 'post-write'">投稿する。</li>
@@ -21,7 +21,7 @@
 					<li v-if="p == 'drive-write'">ドライブを操作する。</li>
 					<li v-if="p == 'notification-read'">通知を見る。</li>
 					<li v-if="p == 'notification-write'">通知を操作する。</li>
-				</virtual>
+				</template>
 			</ul>
 		</section>
 	</div>
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index d71837af4..524d04270 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -18,9 +18,9 @@
 			<p v-if="postsFetching">読み込み中<mk-ellipsis/></p>
 			<div v-if="!postsFetching">
 				<p v-if="posts == null || posts.length == 0">まだ投稿がありません</p>
-				<virtual v-if="posts != null">
+				<template v-if="posts != null">
 					<mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/>
-				</virtual>
+				</template>
 			</div>
 		</div>
 		<hr>
@@ -174,11 +174,11 @@
 		<a v-if="post.reply">&gt;&gt;{ post.reply.index }</a>
 		{ post.text }
 		<div class="media" v-if="post.media">
-			<virtual each={ file in post.media }>
+			<template each={ file in post.media }>
 				<a href={ file.url } target="_blank">
 					<img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
 				</a>
-			</virtual>
+			</template>
 		</div>
 	</div>
 	<style lang="stylus" scoped>
@@ -247,7 +247,7 @@
 		<button @click="selectFile">%fa:upload%%i18n:ch.tags.mk-channel-form.upload%</button>
 		<button @click="drive">%fa:cloud%%i18n:ch.tags.mk-channel-form.drive%</button>
 		<button :class="{ wait: wait }" ref="submit" disabled={ wait || (refs.text.value.length == 0) } @click="post">
-			<virtual v-if="!wait">%fa:paper-plane%</virtual>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.post%' }<mk-ellipsis v-if="wait"/>
+			<template v-if="!wait">%fa:paper-plane%</template>{ wait ? '%i18n:ch.tags.mk-channel-form.posting%' : '%i18n:ch.tags.mk-channel-form.post%' }<mk-ellipsis v-if="wait"/>
 		</button>
 	</div>
 	<mk-uploader ref="uploader"/>
diff --git a/src/web/app/common/tags/error.tag b/src/web/app/common/tags/error.tag
index 6cf13666d..f09c0ce95 100644
--- a/src/web/app/common/tags/error.tag
+++ b/src/web/app/common/tags/error.tag
@@ -98,9 +98,9 @@
 <mk-troubleshooter>
 	<h1>%fa:wrench%%i18n:common.tags.mk-error.troubleshooter.title%</h1>
 	<div>
-		<p data-wip={ network == null }><virtual v-if="network != null"><virtual v-if="network">%fa:check%</virtual><virtual v-if="!network">%fa:times%</virtual></virtual>{ network == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-network%' : '%i18n:common.tags.mk-error.troubleshooter.network%' }<mk-ellipsis v-if="network == null"/></p>
-		<p v-if="network == true" data-wip={ internet == null }><virtual v-if="internet != null"><virtual v-if="internet">%fa:check%</virtual><virtual v-if="!internet">%fa:times%</virtual></virtual>{ internet == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-internet%' : '%i18n:common.tags.mk-error.troubleshooter.internet%' }<mk-ellipsis v-if="internet == null"/></p>
-		<p v-if="internet == true" data-wip={ server == null }><virtual v-if="server != null"><virtual v-if="server">%fa:check%</virtual><virtual v-if="!server">%fa:times%</virtual></virtual>{ server == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-server%' : '%i18n:common.tags.mk-error.troubleshooter.server%' }<mk-ellipsis v-if="server == null"/></p>
+		<p data-wip={ network == null }><template v-if="network != null"><template v-if="network">%fa:check%</template><template v-if="!network">%fa:times%</template></template>{ network == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-network%' : '%i18n:common.tags.mk-error.troubleshooter.network%' }<mk-ellipsis v-if="network == null"/></p>
+		<p v-if="network == true" data-wip={ internet == null }><template v-if="internet != null"><template v-if="internet">%fa:check%</template><template v-if="!internet">%fa:times%</template></template>{ internet == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-internet%' : '%i18n:common.tags.mk-error.troubleshooter.internet%' }<mk-ellipsis v-if="internet == null"/></p>
+		<p v-if="internet == true" data-wip={ server == null }><template v-if="server != null"><template v-if="server">%fa:check%</template><template v-if="!server">%fa:times%</template></template>{ server == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-server%' : '%i18n:common.tags.mk-error.troubleshooter.server%' }<mk-ellipsis v-if="server == null"/></p>
 	</div>
 	<p v-if="!end">%i18n:common.tags.mk-error.troubleshooter.finding%<mk-ellipsis/></p>
 	<p v-if="network === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-network%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-network-desc%</p>
diff --git a/src/web/app/common/tags/file-type-icon.tag b/src/web/app/common/tags/file-type-icon.tag
index a3e479273..f630efe11 100644
--- a/src/web/app/common/tags/file-type-icon.tag
+++ b/src/web/app/common/tags/file-type-icon.tag
@@ -1,5 +1,5 @@
 <mk-file-type-icon>
-	<virtual v-if="kind == 'image'">%fa:file-image%</virtual>
+	<template v-if="kind == 'image'">%fa:file-image%</template>
 	<style lang="stylus" scoped>
 		:scope
 			display inline
diff --git a/src/web/app/common/tags/messaging/form.tag b/src/web/app/common/tags/messaging/form.tag
index e9d2c01ca..9a58dc0ce 100644
--- a/src/web/app/common/tags/messaging/form.tag
+++ b/src/web/app/common/tags/messaging/form.tag
@@ -3,7 +3,7 @@
 	<div class="files"></div>
 	<mk-uploader ref="uploader"/>
 	<button class="send" @click="send" disabled={ sending } title="%i18n:common.send%">
-		<virtual v-if="!sending">%fa:paper-plane%</virtual><virtual v-if="sending">%fa:spinner .spin%</virtual>
+		<template v-if="!sending">%fa:paper-plane%</template><template v-if="sending">%fa:spinner .spin%</template>
 	</button>
 	<button class="attach-from-local" type="button" title="%i18n:common.tags.mk-messaging-form.attach-from-local%">
 		%fa:upload%
diff --git a/src/web/app/common/tags/messaging/index.tag b/src/web/app/common/tags/messaging/index.tag
index 6c25452c0..f7af153c2 100644
--- a/src/web/app/common/tags/messaging/index.tag
+++ b/src/web/app/common/tags/messaging/index.tag
@@ -15,7 +15,7 @@
 		</div>
 	</div>
 	<div class="history" v-if="history.length > 0">
-		<virtual each={ history }>
+		<template each={ history }>
 			<a class="user" data-is-me={ is_me } data-is-read={ is_read } @click="_click">
 				<div>
 					<img class="avatar" src={ (is_me ? recipient.avatar_url : user.avatar_url) + '?thumbnail&size=64' } alt=""/>
@@ -29,7 +29,7 @@
 					</div>
 				</div>
 			</a>
-		</virtual>
+		</template>
 	</div>
 	<p class="no-history" v-if="!fetching && history.length == 0">%i18n:common.tags.mk-messaging.no-history%</p>
 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
diff --git a/src/web/app/common/tags/messaging/message.tag b/src/web/app/common/tags/messaging/message.tag
index 2f193aa5d..ba6d26a18 100644
--- a/src/web/app/common/tags/messaging/message.tag
+++ b/src/web/app/common/tags/messaging/message.tag
@@ -15,7 +15,7 @@
 			</div>
 		</div>
 		<footer>
-			<mk-time time={ message.created_at }/><virtual v-if="message.is_edited">%fa:pencil-alt%</virtual>
+			<mk-time time={ message.created_at }/><template v-if="message.is_edited">%fa:pencil-alt%</template>
 		</footer>
 	</div>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/common/tags/messaging/room.tag b/src/web/app/common/tags/messaging/room.tag
index 91b93c482..990f20a8e 100644
--- a/src/web/app/common/tags/messaging/room.tag
+++ b/src/web/app/common/tags/messaging/room.tag
@@ -4,12 +4,12 @@
 		<p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:common.tags.mk-messaging-room.empty%</p>
 		<p class="no-history" v-if="!init && messages.length > 0 && !moreMessagesIsInStock">%fa:flag%%i18n:common.tags.mk-messaging-room.no-history%</p>
 		<button class="more { fetching: fetchingMoreMessages }" v-if="moreMessagesIsInStock" @click="fetchMoreMessages" disabled={ fetchingMoreMessages }>
-			<virtual v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</virtual>{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' }
+			<template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' }
 		</button>
-		<virtual each={ message, i in messages }>
+		<template each={ message, i in messages }>
 			<mk-messaging-message message={ message }/>
 			<p class="date" v-if="i != messages.length - 1 && message._date != messages[i + 1]._date"><span>{ messages[i + 1]._datetext }</span></p>
-		</virtual>
+		</template>
 	</div>
 	<footer>
 		<div ref="notifications"></div>
diff --git a/src/web/app/common/tags/poll.vue b/src/web/app/common/tags/poll.vue
index 638fa1cbe..0b0132875 100644
--- a/src/web/app/common/tags/poll.vue
+++ b/src/web/app/common/tags/poll.vue
@@ -1,12 +1,12 @@
 <template>
 <div :data-is-voted="isVoted">
 	<ul>
-		<li v-for="choice in poll.choices" @click="vote.bind(choice.id)" :class="{ voted: choice.voted }" title={ !parent.isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', text) : '' }>
-			<div class="backdrop" style={ 'width:' + (parent.result ? (votes / parent.total * 100) : 0) + '%' }></div>
+		<li v-for="choice in poll.choices" :key="choice.id" @click="vote.bind(choice.id)" :class="{ voted: choice.voted }" :title="!choice.isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', choice.text) : ''">
+			<div class="backdrop" :style="{ 'width:' + (result ? (choice.votes / total * 100) : 0) + '%' }"></div>
 			<span>
-				<virtual v-if="is_voted">%fa:check%</virtual>
-				{ text }
-				<span class="votes" v-if="parent.result">({ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', votes) })</span>
+				<template v-if="is_voted">%fa:check%</template>
+				{{ text }}
+				<span class="votes" v-if="parent.result">({{ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', votes) }})</span>
 			</span>
 		</li>
 	</ul>
diff --git a/src/web/app/common/tags/signin-history.tag b/src/web/app/common/tags/signin-history.tag
index cc9d2113f..57ac5ec97 100644
--- a/src/web/app/common/tags/signin-history.tag
+++ b/src/web/app/common/tags/signin-history.tag
@@ -43,8 +43,8 @@
 
 <mk-signin-record>
 	<header @click="toggle">
-		<virtual v-if="rec.success">%fa:check%</virtual>
-		<virtual v-if="!rec.success">%fa:times%</virtual>
+		<template v-if="rec.success">%fa:check%</template>
+		<template v-if="!rec.success">%fa:times%</template>
 		<span class="ip">{ rec.ip }</span>
 		<mk-time time={ rec.created_at }/>
 	</header>
diff --git a/src/web/app/common/tags/signup.tag b/src/web/app/common/tags/signup.tag
index 4e79de787..99be10609 100644
--- a/src/web/app/common/tags/signup.tag
+++ b/src/web/app/common/tags/signup.tag
@@ -29,7 +29,7 @@
 			<p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.password-not-matched%</p>
 		</label>
 		<label class="recaptcha">
-			<p class="caption"><virtual v-if="recaptchaed">%fa:toggle-on%</virtual><virtual v-if="!recaptchaed">%fa:toggle-off%</virtual>%i18n:common.tags.mk-signup.recaptcha%</p>
+			<p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:common.tags.mk-signup.recaptcha%</p>
 			<div v-if="recaptcha" class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" data-sitekey={ recaptcha.site_key }></div>
 		</label>
 		<label class="agree-tou">
diff --git a/src/web/app/desktop/tags/big-follow-button.tag b/src/web/app/desktop/tags/big-follow-button.tag
index 09b587c37..5ea09fdfc 100644
--- a/src/web/app/desktop/tags/big-follow-button.tag
+++ b/src/web/app/desktop/tags/big-follow-button.tag
@@ -2,7 +2,7 @@
 	<button :class="{ wait: wait, follow: !user.is_following, unfollow: user.is_following }" v-if="!init" @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
 		<span v-if="!wait && user.is_following">%fa:minus%フォロー解除</span>
 		<span v-if="!wait && !user.is_following">%fa:plus%フォロー</span>
-		<virtual v-if="wait">%fa:spinner .pulse .fw%</virtual>
+		<template v-if="wait">%fa:spinner .pulse .fw%</template>
 	</button>
 	<div class="init" v-if="init">%fa:spinner .pulse .fw%</div>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/dialog.tag b/src/web/app/desktop/tags/dialog.tag
index cb8c0f31b..ba2fa514d 100644
--- a/src/web/app/desktop/tags/dialog.tag
+++ b/src/web/app/desktop/tags/dialog.tag
@@ -4,9 +4,9 @@
 		<header ref="header"></header>
 		<div class="body" ref="body"></div>
 		<div class="buttons">
-			<virtual each={ opts.buttons }>
+			<template each={ opts.buttons }>
 				<button @click="_onclick">{ text }</button>
-			</virtual>
+			</template>
 		</div>
 	</div>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/tags/drive/browser.tag
index 9fdb27054..02d79afd8 100644
--- a/src/web/app/desktop/tags/drive/browser.tag
+++ b/src/web/app/desktop/tags/drive/browser.tag
@@ -2,10 +2,10 @@
 	<nav>
 		<div class="path" oncontextmenu={ pathOncontextmenu }>
 			<mk-drive-browser-nav-folder :class="{ current: folder == null }" folder={ null }/>
-			<virtual each={ folder in hierarchyFolders }>
+			<template each={ folder in hierarchyFolders }>
 				<span class="separator">%fa:angle-right%</span>
 				<mk-drive-browser-nav-folder folder={ folder }/>
-			</virtual>
+			</template>
 			<span class="separator" v-if="folder != null">%fa:angle-right%</span>
 			<span class="folder current" v-if="folder != null">{ folder.name }</span>
 		</div>
@@ -15,17 +15,17 @@
 		<div class="selection" ref="selection"></div>
 		<div class="contents" ref="contents">
 			<div class="folders" ref="foldersContainer" v-if="folders.length > 0">
-				<virtual each={ folder in folders }>
+				<template each={ folder in folders }>
 					<mk-drive-browser-folder class="folder" folder={ folder }/>
-				</virtual>
+				</template>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 				<div class="padding" each={ Array(10).fill(16) }></div>
 				<button v-if="moreFolders">%i18n:desktop.tags.mk-drive-browser.load-more%</button>
 			</div>
 			<div class="files" ref="filesContainer" v-if="files.length > 0">
-				<virtual each={ file in files }>
+				<template each={ file in files }>
 					<mk-drive-browser-file class="file" file={ file }/>
-				</virtual>
+				</template>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 				<div class="padding" each={ Array(10).fill(16) }></div>
 				<button v-if="moreFiles" @click="fetchMoreFiles">%i18n:desktop.tags.mk-drive-browser.load-more%</button>
diff --git a/src/web/app/desktop/tags/drive/folder.tag b/src/web/app/desktop/tags/drive/folder.tag
index 1ba166a67..ed16bfb0d 100644
--- a/src/web/app/desktop/tags/drive/folder.tag
+++ b/src/web/app/desktop/tags/drive/folder.tag
@@ -1,5 +1,5 @@
 <mk-drive-browser-folder data-is-contextmenu-showing={ isContextmenuShowing.toString() } data-draghover={ draghover.toString() } @click="onclick" onmouseover={ onmouseover } onmouseout={ onmouseout } ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop } oncontextmenu={ oncontextmenu } draggable="true" ondragstart={ ondragstart } ondragend={ ondragend } title={ title }>
-	<p class="name"><virtual v-if="hover">%fa:R folder-open .fw%</virtual><virtual v-if="!hover">%fa:R folder .fw%</virtual>{ folder.name }</p>
+	<p class="name"><template v-if="hover">%fa:R folder-open .fw%</template><template v-if="!hover">%fa:R folder .fw%</template>{ folder.name }</p>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/drive/nav-folder.tag b/src/web/app/desktop/tags/drive/nav-folder.tag
index 2afbb50f0..4bca80f68 100644
--- a/src/web/app/desktop/tags/drive/nav-folder.tag
+++ b/src/web/app/desktop/tags/drive/nav-folder.tag
@@ -1,5 +1,5 @@
 <mk-drive-browser-nav-folder data-draghover={ draghover } @click="onclick" ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop }>
-	<virtual v-if="folder == null">%fa:cloud%</virtual><span>{ folder == null ? '%i18n:desktop.tags.mk-drive-browser-nav-folder.drive%' : folder.name }</span>
+	<template v-if="folder == null">%fa:cloud%</template><span>{ folder == null ? '%i18n:desktop.tags.mk-drive-browser-nav-folder.drive%' : folder.name }</span>
 	<style lang="stylus" scoped>
 		:scope
 			&[data-draghover]
diff --git a/src/web/app/desktop/tags/follow-button.tag b/src/web/app/desktop/tags/follow-button.tag
index 9a01b0831..fa7d43e03 100644
--- a/src/web/app/desktop/tags/follow-button.tag
+++ b/src/web/app/desktop/tags/follow-button.tag
@@ -1,8 +1,8 @@
 <mk-follow-button>
 	<button :class="{ wait: wait, follow: !user.is_following, unfollow: user.is_following }" v-if="!init" @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
-		<virtual v-if="!wait && user.is_following">%fa:minus%</virtual>
-		<virtual v-if="!wait && !user.is_following">%fa:plus%</virtual>
-		<virtual v-if="wait">%fa:spinner .pulse .fw%</virtual>
+		<template v-if="!wait && user.is_following">%fa:minus%</template>
+		<template v-if="!wait && !user.is_following">%fa:plus%</template>
+		<template v-if="wait">%fa:spinner .pulse .fw%</template>
 	</button>
 	<div class="init" v-if="init">%fa:spinner .pulse .fw%</div>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/home-widgets/access-log.tag b/src/web/app/desktop/tags/home-widgets/access-log.tag
index c3adc0d8b..fea18299e 100644
--- a/src/web/app/desktop/tags/home-widgets/access-log.tag
+++ b/src/web/app/desktop/tags/home-widgets/access-log.tag
@@ -1,7 +1,7 @@
 <mk-access-log-home-widget>
-	<virtual v-if="data.design == 0">
+	<template v-if="data.design == 0">
 		<p class="title">%fa:server%%i18n:desktop.tags.mk-access-log-home-widget.title%</p>
-	</virtual>
+	</template>
 	<div ref="log">
 		<p each={ requests }>
 			<span class="ip" style="color:{ fg }; background:{ bg }">{ ip }</span>
diff --git a/src/web/app/desktop/tags/home-widgets/broadcast.tag b/src/web/app/desktop/tags/home-widgets/broadcast.tag
index e1ba82e79..91ddbb4ab 100644
--- a/src/web/app/desktop/tags/home-widgets/broadcast.tag
+++ b/src/web/app/desktop/tags/home-widgets/broadcast.tag
@@ -12,7 +12,7 @@
 	<h1 v-if="!fetching">{
 		broadcasts.length == 0 ? '%i18n:desktop.tags.mk-broadcast-home-widget.no-broadcasts%' : broadcasts[i].title
 	}</h1>
-	<p v-if="!fetching"><mk-raw v-if="broadcasts.length != 0" content={ broadcasts[i].text }/><virtual v-if="broadcasts.length == 0">%i18n:desktop.tags.mk-broadcast-home-widget.have-a-nice-day%</virtual></p>
+	<p v-if="!fetching"><mk-raw v-if="broadcasts.length != 0" content={ broadcasts[i].text }/><template v-if="broadcasts.length == 0">%i18n:desktop.tags.mk-broadcast-home-widget.have-a-nice-day%</template></p>
 	<a v-if="broadcasts.length > 1" @click="next">%i18n:desktop.tags.mk-broadcast-home-widget.next% &gt;&gt;</a>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/desktop/tags/home-widgets/channel.tag b/src/web/app/desktop/tags/home-widgets/channel.tag
index 0b4fbbf4f..98bf6bf7e 100644
--- a/src/web/app/desktop/tags/home-widgets/channel.tag
+++ b/src/web/app/desktop/tags/home-widgets/channel.tag
@@ -1,10 +1,10 @@
 <mk-channel-home-widget>
-	<virtual v-if="!data.compact">
+	<template v-if="!data.compact">
 		<p class="title">%fa:tv%{
 			channel ? channel.title : '%i18n:desktop.tags.mk-channel-home-widget.title%'
 		}</p>
 		<button @click="settings" title="%i18n:desktop.tags.mk-channel-home-widget.settings%">%fa:cog%</button>
-	</virtual>
+	</template>
 	<p class="get-started" v-if="this.data.channel == null">%i18n:desktop.tags.mk-channel-home-widget.get-started%</p>
 	<mk-channel ref="channel" show={ this.data.channel }/>
 	<style lang="stylus" scoped>
@@ -200,11 +200,11 @@
 		<a v-if="post.reply">&gt;&gt;{ post.reply.index }</a>
 		{ post.text }
 		<div class="media" v-if="post.media">
-			<virtual each={ file in post.media }>
+			<template each={ file in post.media }>
 				<a href={ file.url } target="_blank">
 					<img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
 				</a>
-			</virtual>
+			</template>
 		</div>
 	</div>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/tags/home-widgets/mentions.tag
index 2ca1fa502..e0592aa04 100644
--- a/src/web/app/desktop/tags/home-widgets/mentions.tag
+++ b/src/web/app/desktop/tags/home-widgets/mentions.tag
@@ -6,8 +6,8 @@
 	<p class="empty" v-if="isEmpty">%fa:R comments%<span v-if="mode == 'all'">あなた宛ての投稿はありません。</span><span v-if="mode == 'following'">あなたがフォローしているユーザーからの言及はありません。</span></p>
 	<mk-timeline ref="timeline">
 		<yield to="footer">
-			<virtual v-if="!parent.moreLoading">%fa:moon%</virtual>
-			<virtual v-if="parent.moreLoading">%fa:spinner .pulse .fw%</virtual>
+			<template v-if="!parent.moreLoading">%fa:moon%</template>
+			<template v-if="parent.moreLoading">%fa:spinner .pulse .fw%</template>
 		</yield/>
 	</mk-timeline>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/home-widgets/messaging.tag b/src/web/app/desktop/tags/home-widgets/messaging.tag
index cd11c21a2..d3b77b58c 100644
--- a/src/web/app/desktop/tags/home-widgets/messaging.tag
+++ b/src/web/app/desktop/tags/home-widgets/messaging.tag
@@ -1,7 +1,7 @@
 <mk-messaging-home-widget>
-	<virtual v-if="data.design == 0">
+	<template v-if="data.design == 0">
 		<p class="title">%fa:comments%%i18n:desktop.tags.mk-messaging-home-widget.title%</p>
-	</virtual>
+	</template>
 	<mk-messaging ref="index" compact={ true }/>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/desktop/tags/home-widgets/notifications.tag b/src/web/app/desktop/tags/home-widgets/notifications.tag
index 4c48da659..bd915b197 100644
--- a/src/web/app/desktop/tags/home-widgets/notifications.tag
+++ b/src/web/app/desktop/tags/home-widgets/notifications.tag
@@ -1,8 +1,8 @@
 <mk-notifications-home-widget>
-	<virtual v-if="!data.compact">
+	<template v-if="!data.compact">
 		<p class="title">%fa:R bell%%i18n:desktop.tags.mk-notifications-home-widget.title%</p>
 		<button @click="settings" title="%i18n:desktop.tags.mk-notifications-home-widget.settings%">%fa:cog%</button>
-	</virtual>
+	</template>
 	<mk-notifications/>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/desktop/tags/home-widgets/photo-stream.tag b/src/web/app/desktop/tags/home-widgets/photo-stream.tag
index 8c57dbbef..a2d95dede 100644
--- a/src/web/app/desktop/tags/home-widgets/photo-stream.tag
+++ b/src/web/app/desktop/tags/home-widgets/photo-stream.tag
@@ -1,12 +1,12 @@
 <mk-photo-stream-home-widget data-melt={ data.design == 2 }>
-	<virtual v-if="data.design == 0">
+	<template v-if="data.design == 0">
 		<p class="title">%fa:camera%%i18n:desktop.tags.mk-photo-stream-home-widget.title%</p>
-	</virtual>
+	</template>
 	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<div class="stream" v-if="!initializing && images.length > 0">
-		<virtual each={ image in images }>
+		<template each={ image in images }>
 			<div class="img" style={ 'background-image: url(' + image.url + '?thumbnail&size=256)' }></div>
-		</virtual>
+		</template>
 	</div>
 	<p class="empty" v-if="!initializing && images.length == 0">%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/home-widgets/post-form.tag b/src/web/app/desktop/tags/home-widgets/post-form.tag
index 58ceac604..d5824477b 100644
--- a/src/web/app/desktop/tags/home-widgets/post-form.tag
+++ b/src/web/app/desktop/tags/home-widgets/post-form.tag
@@ -1,12 +1,12 @@
 <mk-post-form-home-widget>
 	<mk-post-form v-if="place == 'main'"/>
-	<virtual v-if="place != 'main'">
-		<virtual v-if="data.design == 0">
+	<template v-if="place != 'main'">
+		<template v-if="data.design == 0">
 			<p class="title">%fa:pencil-alt%%i18n:desktop.tags.mk-post-form-home-widget.title%</p>
-		</virtual>
+		</template>
 		<textarea disabled={ posting } ref="text" onkeydown={ onkeydown } placeholder="%i18n:desktop.tags.mk-post-form-home-widget.placeholder%"></textarea>
 		<button @click="post" disabled={ posting }>%i18n:desktop.tags.mk-post-form-home-widget.post%</button>
-	</virtual>
+	</template>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag b/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
index f33b2de5f..cfbcd1e92 100644
--- a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
+++ b/src/web/app/desktop/tags/home-widgets/recommended-polls.tag
@@ -1,8 +1,8 @@
 <mk-recommended-polls-home-widget>
-	<virtual v-if="!data.compact">
+	<template v-if="!data.compact">
 		<p class="title">%fa:chart-pie%%i18n:desktop.tags.mk-recommended-polls-home-widget.title%</p>
 		<button @click="fetch" title="%i18n:desktop.tags.mk-recommended-polls-home-widget.refresh%">%fa:sync%</button>
-	</virtual>
+	</template>
 	<div class="poll" v-if="!loading && poll != null">
 		<p v-if="poll.text"><a href="/{ poll.user.username }/{ poll.id }">{ poll.text }</a></p>
 		<p v-if="!poll.text"><a href="/{ poll.user.username }/{ poll.id }">%fa:link%</a></p>
diff --git a/src/web/app/desktop/tags/home-widgets/rss-reader.tag b/src/web/app/desktop/tags/home-widgets/rss-reader.tag
index f8a0787d3..4e0ed702e 100644
--- a/src/web/app/desktop/tags/home-widgets/rss-reader.tag
+++ b/src/web/app/desktop/tags/home-widgets/rss-reader.tag
@@ -1,10 +1,10 @@
 <mk-rss-reader-home-widget>
-	<virtual v-if="!data.compact">
+	<template v-if="!data.compact">
 		<p class="title">%fa:rss-square%RSS</p>
 		<button @click="settings" title="設定">%fa:cog%</button>
-	</virtual>
+	</template>
 	<div class="feed" v-if="!initializing">
-		<virtual each={ item in items }><a href={ item.link } target="_blank">{ item.title }</a></virtual>
+		<template each={ item in items }><a href={ item.link } target="_blank">{ item.title }</a></template>
 	</div>
 	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/home-widgets/server.tag b/src/web/app/desktop/tags/home-widgets/server.tag
index 1a15d3704..992517163 100644
--- a/src/web/app/desktop/tags/home-widgets/server.tag
+++ b/src/web/app/desktop/tags/home-widgets/server.tag
@@ -1,8 +1,8 @@
 <mk-server-home-widget data-melt={ data.design == 2 }>
-	<virtual v-if="data.design == 0">
+	<template v-if="data.design == 0">
 		<p class="title">%fa:server%%i18n:desktop.tags.mk-server-home-widget.title%</p>
 		<button @click="toggle" title="%i18n:desktop.tags.mk-server-home-widget.toggle%">%fa:sort%</button>
-	</virtual>
+	</template>
 	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<mk-server-home-widget-cpu-and-memory-usage v-if="!initializing" show={ data.view == 0 } connection={ connection }/>
 	<mk-server-home-widget-cpu v-if="!initializing" show={ data.view == 1 } connection={ connection } meta={ meta }/>
diff --git a/src/web/app/desktop/tags/home-widgets/timeline.tag b/src/web/app/desktop/tags/home-widgets/timeline.tag
index 67e56b676..ac2d95d5a 100644
--- a/src/web/app/desktop/tags/home-widgets/timeline.tag
+++ b/src/web/app/desktop/tags/home-widgets/timeline.tag
@@ -6,8 +6,8 @@
 	<p class="empty" v-if="isEmpty && !isLoading">%fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。</p>
 	<mk-timeline ref="timeline" hide={ isLoading }>
 		<yield to="footer">
-			<virtual v-if="!parent.moreLoading">%fa:moon%</virtual>
-			<virtual v-if="parent.moreLoading">%fa:spinner .pulse .fw%</virtual>
+			<template v-if="!parent.moreLoading">%fa:moon%</template>
+			<template v-if="parent.moreLoading">%fa:spinner .pulse .fw%</template>
 		</yield/>
 	</mk-timeline>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/home-widgets/trends.tag b/src/web/app/desktop/tags/home-widgets/trends.tag
index 4e5060a3e..5e297ebc7 100644
--- a/src/web/app/desktop/tags/home-widgets/trends.tag
+++ b/src/web/app/desktop/tags/home-widgets/trends.tag
@@ -1,8 +1,8 @@
 <mk-trends-home-widget>
-	<virtual v-if="!data.compact">
+	<template v-if="!data.compact">
 		<p class="title">%fa:fire%%i18n:desktop.tags.mk-trends-home-widget.title%</p>
 		<button @click="fetch" title="%i18n:desktop.tags.mk-trends-home-widget.refresh%">%fa:sync%</button>
-	</virtual>
+	</template>
 	<div class="post" v-if="!loading && post != null">
 		<p class="text"><a href="/{ post.user.username }/{ post.id }">{ post.text }</a></p>
 		<p class="author">―<a href="/{ post.user.username }">@{ post.user.username }</a></p>
diff --git a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
index fb23eac5e..5344da1f2 100644
--- a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
+++ b/src/web/app/desktop/tags/home-widgets/user-recommendation.tag
@@ -1,8 +1,8 @@
 <mk-user-recommendation-home-widget>
-	<virtual v-if="!data.compact">
+	<template v-if="!data.compact">
 		<p class="title">%fa:users%%i18n:desktop.tags.mk-user-recommendation-home-widget.title%</p>
 		<button @click="refresh" title="%i18n:desktop.tags.mk-user-recommendation-home-widget.refresh%">%fa:sync%</button>
-	</virtual>
+	</template>
 	<div class="user" v-if="!loading && users.length != 0" each={ _user in users }>
 		<a class="avatar-anchor" href={ '/' + _user.username }>
 			<img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ _user.id }/>
diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
index 594c706be..1094e0d96 100644
--- a/src/web/app/desktop/tags/images.tag
+++ b/src/web/app/desktop/tags/images.tag
@@ -1,7 +1,7 @@
 <mk-images>
-	<virtual each={ image in images }>
+	<template each={ image in images }>
 		<mk-images-image image={ image }/>
-	</virtual>
+	</template>
 	<style lang="stylus" scoped>
 		:scope
 			display grid
diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag
index 7bba90a8b..a599e5d6a 100644
--- a/src/web/app/desktop/tags/notifications.tag
+++ b/src/web/app/desktop/tags/notifications.tag
@@ -1,9 +1,9 @@
 <mk-notifications>
 	<div class="notifications" v-if="notifications.length != 0">
-		<virtual each={ notification, i in notifications }>
+		<template each={ notification, i in notifications }>
 			<div class="notification { notification.type }">
 				<mk-time time={ notification.created_at }/>
-				<virtual v-if="notification.type == 'reaction'">
+				<template v-if="notification.type == 'reaction'">
 					<a class="avatar-anchor" href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>
 						<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
 					</a>
@@ -13,8 +13,8 @@
 							%fa:quote-left%{ getPostSummary(notification.post) }%fa:quote-right%
 						</a>
 					</div>
-				</virtual>
-				<virtual v-if="notification.type == 'repost'">
+				</template>
+				<template v-if="notification.type == 'repost'">
 					<a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>
 						<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
 					</a>
@@ -24,8 +24,8 @@
 							%fa:quote-left%{ getPostSummary(notification.post.repost) }%fa:quote-right%
 						</a>
 					</div>
-				</virtual>
-				<virtual v-if="notification.type == 'quote'">
+				</template>
+				<template v-if="notification.type == 'quote'">
 					<a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>
 						<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
 					</a>
@@ -33,16 +33,16 @@
 						<p>%fa:quote-left%<a href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>{ notification.post.user.name }</a></p>
 						<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
 					</div>
-				</virtual>
-				<virtual v-if="notification.type == 'follow'">
+				</template>
+				<template v-if="notification.type == 'follow'">
 					<a class="avatar-anchor" href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>
 						<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
 					</a>
 					<div class="text">
 						<p>%fa:user-plus%<a href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>{ notification.user.name }</a></p>
 					</div>
-				</virtual>
-				<virtual v-if="notification.type == 'reply'">
+				</template>
+				<template v-if="notification.type == 'reply'">
 					<a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>
 						<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
 					</a>
@@ -50,8 +50,8 @@
 						<p>%fa:reply%<a href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>{ notification.post.user.name }</a></p>
 						<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
 					</div>
-				</virtual>
-				<virtual v-if="notification.type == 'mention'">
+				</template>
+				<template v-if="notification.type == 'mention'">
 					<a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>
 						<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
 					</a>
@@ -59,8 +59,8 @@
 						<p>%fa:at%<a href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>{ notification.post.user.name }</a></p>
 						<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
 					</div>
-				</virtual>
-				<virtual v-if="notification.type == 'poll_vote'">
+				</template>
+				<template v-if="notification.type == 'poll_vote'">
 					<a class="avatar-anchor" href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>
 						<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
 					</a>
@@ -70,16 +70,16 @@
 							%fa:quote-left%{ getPostSummary(notification.post) }%fa:quote-right%
 						</a>
 					</div>
-				</virtual>
+				</template>
 			</div>
 			<p class="date" v-if="i != notifications.length - 1 && notification._date != notifications[i + 1]._date">
 				<span>%fa:angle-up%{ notification._datetext }</span>
 				<span>%fa:angle-down%{ notifications[i + 1]._datetext }</span>
 			</p>
-		</virtual>
+		</template>
 	</div>
 	<button class="more { fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" disabled={ fetchingMoreNotifications }>
-		<virtual v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</virtual>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:desktop.tags.mk-notifications.more%' }
+		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:desktop.tags.mk-notifications.more%' }
 	</button>
 	<p class="empty" v-if="notifications.length == 0 && !loading">ありません!</p>
 	<p class="loading" v-if="loading">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index 2225733f7..5f35ce6af 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -1,13 +1,13 @@
 <mk-post-detail title={ title }>
 	<div class="main">
 		<button class="read-more" v-if="p.reply && p.reply.reply_id && context == null" title="会話をもっと読み込む" @click="loadContext" disabled={ contextFetching }>
-			<virtual v-if="!contextFetching">%fa:ellipsis-v%</virtual>
-			<virtual v-if="contextFetching">%fa:spinner .pulse%</virtual>
+			<template v-if="!contextFetching">%fa:ellipsis-v%</template>
+			<template v-if="contextFetching">%fa:spinner .pulse%</template>
 		</button>
 		<div class="context">
-			<virtual each={ post in context }>
+			<template each={ post in context }>
 				<mk-post-detail-sub post={ post }/>
-			</virtual>
+			</template>
 		</div>
 		<div class="reply-to" v-if="p.reply">
 			<mk-post-detail-sub post={ p.reply }/>
@@ -58,9 +58,9 @@
 			</footer>
 		</article>
 		<div class="replies" v-if="!compact">
-			<virtual each={ post in replies }>
+			<template each={ post in replies }>
 				<mk-post-detail-sub post={ post }/>
-			</virtual>
+			</template>
 		</div>
 	</div>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/repost-form.tag b/src/web/app/desktop/tags/repost-form.tag
index 77118124c..a3d350fa2 100644
--- a/src/web/app/desktop/tags/repost-form.tag
+++ b/src/web/app/desktop/tags/repost-form.tag
@@ -1,15 +1,15 @@
 <mk-repost-form>
 	<mk-post-preview post={ opts.post }/>
-	<virtual v-if="!quote">
+	<template v-if="!quote">
 		<footer>
 			<a class="quote" v-if="!quote" @click="onquote">%i18n:desktop.tags.mk-repost-form.quote%</a>
 			<button class="cancel" @click="cancel">%i18n:desktop.tags.mk-repost-form.cancel%</button>
 			<button class="ok" @click="ok" disabled={ wait }>{ wait ? '%i18n:desktop.tags.mk-repost-form.reposting%' : '%i18n:desktop.tags.mk-repost-form.repost%' }</button>
 		</footer>
-	</virtual>
-	<virtual v-if="quote">
+	</template>
+	<template v-if="quote">
 		<mk-post-form ref="form" repost={ opts.post }/>
-	</virtual>
+	</template>
 	<style lang="stylus" scoped>
 		:scope
 
diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag
index 09320c5d7..91bea2e90 100644
--- a/src/web/app/desktop/tags/search-posts.tag
+++ b/src/web/app/desktop/tags/search-posts.tag
@@ -5,8 +5,8 @@
 	<p class="empty" v-if="isEmpty">%fa:search%「{ query }」に関する投稿は見つかりませんでした。</p>
 	<mk-timeline ref="timeline">
 		<yield to="footer">
-			<virtual v-if="!parent.moreLoading">%fa:moon%</virtual>
-			<virtual v-if="parent.moreLoading">%fa:spinner .pulse .fw%</virtual>
+			<template v-if="!parent.moreLoading">%fa:moon%</template>
+			<template v-if="parent.moreLoading">%fa:spinner .pulse .fw%</template>
 		</yield/>
 	</mk-timeline>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 191d1d754..4bf210cef 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -266,10 +266,10 @@
 	<p>%i18n:desktop.tags.mk-2fa-setting.intro%<a href="%i18n:desktop.tags.mk-2fa-setting.url%" target="_blank">%i18n:desktop.tags.mk-2fa-setting.detail%</a></p>
 	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-2fa-setting.caution%</p></div>
 	<p v-if="!data && !I.two_factor_enabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
-	<virtual v-if="I.two_factor_enabled">
+	<template v-if="I.two_factor_enabled">
 		<p>%i18n:desktop.tags.mk-2fa-setting.already-registered%</p>
 		<button @click="unregister" class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button>
-	</virtual>
+	</template>
 	<div v-if="data">
 		<ol>
 			<li>%i18n:desktop.tags.mk-2fa-setting.authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:desktop.tags.mk-2fa-setting.howtoinstall%</a></li>
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 772140dcc..7f79d18b4 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -1,8 +1,8 @@
 <mk-timeline>
-	<virtual each={ post, i in posts }>
+	<template each={ post, i in posts }>
 		<mk-timeline-post post={ post }/>
 		<p class="date" v-if="i != posts.length - 1 && post._date != posts[i + 1]._date"><span>%fa:angle-up%{ post._datetext }</span><span>%fa:angle-down%{ posts[i + 1]._datetext }</span></p>
-	</virtual>
+	</template>
 	<footer data-yield="footer">
 		<yield from="footer"/>
 	</footer>
@@ -142,8 +142,8 @@
 					%fa:ellipsis-h%
 				</button>
 				<button @click="toggleDetail" title="%i18n:desktop.tags.mk-timeline-post.detail">
-					<virtual v-if="!isDetailOpened">%fa:caret-down%</virtual>
-					<virtual v-if="isDetailOpened">%fa:caret-up%</virtual>
+					<template v-if="!isDetailOpened">%fa:caret-down%</template>
+					<template v-if="isDetailOpened">%fa:caret-up%</template>
 				</button>
 			</footer>
 		</div>
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index 0a3849236..e5008b838 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -230,7 +230,7 @@
 
 <mk-ui-header-notifications>
 	<button data-active={ isOpen } @click="toggle" title="%i18n:desktop.tags.mk-ui-header-notifications.title%">
-		%fa:R bell%<virtual v-if="hasUnreadNotifications">%fa:circle%</virtual>
+		%fa:R bell%<template v-if="hasUnreadNotifications">%fa:circle%</template>
 	</button>
 	<div class="notifications" v-if="isOpen">
 		<mk-notifications/>
@@ -392,7 +392,7 @@
 
 <mk-ui-header-nav>
 	<ul>
-		<virtual v-if="SIGNIN">
+		<template v-if="SIGNIN">
 			<li class="home { active: page == 'home' }">
 				<a href={ _URL_ }>
 					%fa:home%
@@ -403,10 +403,10 @@
 				<a @click="messaging">
 					%fa:comments%
 					<p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p>
-					<virtual v-if="hasUnreadMessagingMessages">%fa:circle%</virtual>
+					<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>
 				</a>
 			</li>
-		</virtual>
+		</template>
 		<li class="ch">
 			<a href={ _CH_URL_ } target="_blank">
 				%fa:tv%
@@ -630,7 +630,7 @@
 
 <mk-ui-header-account>
 	<button class="header" data-active={ isOpen.toString() } @click="toggle">
-		<span class="username">{ I.username }<virtual v-if="!isOpen">%fa:angle-down%</virtual><virtual v-if="isOpen">%fa:angle-up%</virtual></span>
+		<span class="username">{ I.username }<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
 		<img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 	</button>
 	<div class="menu" v-if="isOpen">
diff --git a/src/web/app/desktop/tags/user-preview.tag b/src/web/app/desktop/tags/user-preview.tag
index 00ecfba1b..10c37de64 100644
--- a/src/web/app/desktop/tags/user-preview.tag
+++ b/src/web/app/desktop/tags/user-preview.tag
@@ -1,5 +1,5 @@
 <mk-user-preview>
-	<virtual v-if="user != null">
+	<template v-if="user != null">
 		<div class="banner" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=512)' : '' }></div><a class="avatar" href={ '/' + user.username } target="_blank"><img src={ user.avatar_url + '?thumbnail&size=64' } alt="avatar"/></a>
 		<div class="title">
 			<p class="name">{ user.name }</p>
@@ -18,7 +18,7 @@
 			</div>
 		</div>
 		<mk-follow-button v-if="SIGNIN && user.id != I.id" user={ userPromise }/>
-	</virtual>
+	</template>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/tags/user-timeline.tag
index 3baf5db0e..2e3bbbfd6 100644
--- a/src/web/app/desktop/tags/user-timeline.tag
+++ b/src/web/app/desktop/tags/user-timeline.tag
@@ -8,8 +8,8 @@
 	<p class="empty" v-if="isEmpty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p>
 	<mk-timeline ref="timeline">
 		<yield to="footer">
-			<virtual v-if="!parent.moreLoading">%fa:moon%</virtual>
-			<virtual v-if="parent.moreLoading">%fa:spinner .pulse .fw%</virtual>
+			<template v-if="!parent.moreLoading">%fa:moon%</template>
+			<template v-if="parent.moreLoading">%fa:spinner .pulse .fw%</template>
 		</yield/>
 	</mk-timeline>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/tags/user.tag
index daf39347f..161a15190 100644
--- a/src/web/app/desktop/tags/user.tag
+++ b/src/web/app/desktop/tags/user.tag
@@ -357,9 +357,9 @@
 	<p class="title">%fa:camera%%i18n:desktop.tags.mk-user.photos.title%</p>
 	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.photos.loading%<mk-ellipsis/></p>
 	<div class="stream" v-if="!initializing && images.length > 0">
-		<virtual each={ image in images }>
+		<template each={ image in images }>
 			<div class="img" style={ 'background-image: url(' + image.url + '?thumbnail&size=256)' }></div>
-		</virtual>
+		</template>
 	</div>
 	<p class="empty" v-if="!initializing && images.length == 0">%i18n:desktop.tags.mk-user.photos.no-photos%</p>
 	<style lang="stylus" scoped>
@@ -563,9 +563,9 @@
 	<p class="title">%fa:users%%i18n:desktop.tags.mk-user.followers-you-know.title%</p>
 	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p>
 	<div v-if="!initializing && users.length > 0">
-	<virtual each={ user in users }>
+	<template each={ user in users }>
 		<a href={ '/' + user.username }><img src={ user.avatar_url + '?thumbnail&size=64' } alt={ user.name }/></a>
-	</virtual>
+	</template>
 	</div>
 	<p class="empty" v-if="!initializing && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/tags/widgets/activity.tag b/src/web/app/desktop/tags/widgets/activity.tag
index 03d253ea2..ffddfa7dc 100644
--- a/src/web/app/desktop/tags/widgets/activity.tag
+++ b/src/web/app/desktop/tags/widgets/activity.tag
@@ -1,8 +1,8 @@
 <mk-activity-widget data-melt={ design == 2 }>
-	<virtual v-if="design == 0">
+	<template v-if="design == 0">
 		<p class="title">%fa:chart-bar%%i18n:desktop.tags.mk-activity-widget.title%</p>
 		<button @click="toggle" title="%i18n:desktop.tags.mk-activity-widget.toggle%">%fa:sort%</button>
-	</virtual>
+	</template>
 	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<mk-activity-widget-calender v-if="!initializing && view == 0" data={ [].concat(activity) }/>
 	<mk-activity-widget-chart v-if="!initializing && view == 1" data={ [].concat(activity) }/>
diff --git a/src/web/app/desktop/tags/widgets/calendar.tag b/src/web/app/desktop/tags/widgets/calendar.tag
index 3d2d84e40..d20180f1c 100644
--- a/src/web/app/desktop/tags/widgets/calendar.tag
+++ b/src/web/app/desktop/tags/widgets/calendar.tag
@@ -1,9 +1,9 @@
 <mk-calendar-widget data-melt={ opts.design == 4 || opts.design == 5 }>
-	<virtual v-if="opts.design == 0 || opts.design == 1">
+	<template v-if="opts.design == 0 || opts.design == 1">
 		<button @click="prev" title="%i18n:desktop.tags.mk-calendar-widget.prev%">%fa:chevron-circle-left%</button>
 		<p class="title">{ '%i18n:desktop.tags.mk-calendar-widget.title%'.replace('{1}', year).replace('{2}', month) }</p>
 		<button @click="next" title="%i18n:desktop.tags.mk-calendar-widget.next%">%fa:chevron-circle-right%</button>
-	</virtual>
+	</template>
 
 	<div class="calendar">
 		<div class="weekday" v-if="opts.design == 0 || opts.design == 2 || opts.design == 4} each={ day, i in Array(7).fill(0)"
diff --git a/src/web/app/dev/tags/pages/apps.tag b/src/web/app/dev/tags/pages/apps.tag
index f7b8e416e..bf9552f07 100644
--- a/src/web/app/dev/tags/pages/apps.tag
+++ b/src/web/app/dev/tags/pages/apps.tag
@@ -2,13 +2,13 @@
 	<h1>アプリを管理</h1><a href="/app/new">アプリ作成</a>
 	<div class="apps">
 		<p v-if="fetching">読み込み中</p>
-		<virtual v-if="!fetching">
+		<template v-if="!fetching">
 			<p v-if="apps.length == 0">アプリなし</p>
 			<ul v-if="apps.length > 0">
 				<li each={ app in apps }><a href={ '/app/' + app.id }>
 						<p class="name">{ app.name }</p></a></li>
 			</ul>
-		</virtual>
+		</template>
 	</div>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index b5e428665..50578299a 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -1,39 +1,39 @@
 <mk-drive>
 	<nav ref="nav">
 		<a @click="goRoot" href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</a>
-		<virtual each={ folder in hierarchyFolders }>
+		<template each={ folder in hierarchyFolders }>
 			<span>%fa:angle-right%</span>
 			<a @click="move" href="/i/drive/folder/{ folder.id }">{ folder.name }</a>
-		</virtual>
-		<virtual v-if="folder != null">
+		</template>
+		<template v-if="folder != null">
 			<span>%fa:angle-right%</span>
 			<p>{ folder.name }</p>
-		</virtual>
-		<virtual v-if="file != null">
+		</template>
+		<template v-if="file != null">
 			<span>%fa:angle-right%</span>
 			<p>{ file.name }</p>
-		</virtual>
+		</template>
 	</nav>
 	<mk-uploader ref="uploader"/>
 	<div class="browser { fetching: fetching }" v-if="file == null">
 		<div class="info" v-if="info">
 			<p v-if="folder == null">{ (info.usage / info.capacity * 100).toFixed(1) }% %i18n:mobile.tags.mk-drive.used%</p>
 			<p v-if="folder != null && (folder.folders_count > 0 || folder.files_count > 0)">
-				<virtual v-if="folder.folders_count > 0">{ folder.folders_count } %i18n:mobile.tags.mk-drive.folder-count%</virtual>
-				<virtual v-if="folder.folders_count > 0 && folder.files_count > 0">%i18n:mobile.tags.mk-drive.count-separator%</virtual>
-				<virtual v-if="folder.files_count > 0">{ folder.files_count } %i18n:mobile.tags.mk-drive.file-count%</virtual>
+				<template v-if="folder.folders_count > 0">{ folder.folders_count } %i18n:mobile.tags.mk-drive.folder-count%</template>
+				<template v-if="folder.folders_count > 0 && folder.files_count > 0">%i18n:mobile.tags.mk-drive.count-separator%</template>
+				<template v-if="folder.files_count > 0">{ folder.files_count } %i18n:mobile.tags.mk-drive.file-count%</template>
 			</p>
 		</div>
 		<div class="folders" v-if="folders.length > 0">
-			<virtual each={ folder in folders }>
+			<template each={ folder in folders }>
 				<mk-drive-folder folder={ folder }/>
-			</virtual>
+			</template>
 			<p v-if="moreFolders">%i18n:mobile.tags.mk-drive.load-more%</p>
 		</div>
 		<div class="files" v-if="files.length > 0">
-			<virtual each={ file in files }>
+			<template each={ file in files }>
 				<mk-drive-file file={ file }/>
-			</virtual>
+			</template>
 			<button class="more" v-if="moreFiles" @click="fetchMoreFiles">
 				{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-drive.load-more%' }
 			</button>
diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag
index 846d12d86..ab0c94ae9 100644
--- a/src/web/app/mobile/tags/drive/file-viewer.tag
+++ b/src/web/app/mobile/tags/drive/file-viewer.tag
@@ -6,7 +6,7 @@
 			title={ file.name }
 			onload={ onImageLoaded }
 			style="background-color:rgb({ file.properties.average_color.join(',') })">
-		<virtual v-if="kind != 'image'">%fa:file%</virtual>
+		<template v-if="kind != 'image'">%fa:file%</template>
 		<footer v-if="kind == 'image' && file.properties && file.properties.width && file.properties.height">
 			<span class="size">
 				<span class="width">{ file.properties.width }</span>
diff --git a/src/web/app/mobile/tags/follow-button.tag b/src/web/app/mobile/tags/follow-button.tag
index 5f746c46b..c6215a7ba 100644
--- a/src/web/app/mobile/tags/follow-button.tag
+++ b/src/web/app/mobile/tags/follow-button.tag
@@ -1,8 +1,8 @@
 <mk-follow-button>
 	<button :class="{ wait: wait, follow: !user.is_following, unfollow: user.is_following }" v-if="!init" @click="onclick" disabled={ wait }>
-		<virtual v-if="!wait && user.is_following">%fa:minus%</virtual>
-		<virtual v-if="!wait && !user.is_following">%fa:plus%</virtual>
-		<virtual v-if="wait">%fa:spinner .pulse .fw%</virtual>{ user.is_following ? '%i18n:mobile.tags.mk-follow-button.unfollow%' : '%i18n:mobile.tags.mk-follow-button.follow%' }
+		<template v-if="!wait && user.is_following">%fa:minus%</template>
+		<template v-if="!wait && !user.is_following">%fa:plus%</template>
+		<template v-if="wait">%fa:spinner .pulse .fw%</template>{ user.is_following ? '%i18n:mobile.tags.mk-follow-button.unfollow%' : '%i18n:mobile.tags.mk-follow-button.follow%' }
 	</button>
 	<div class="init" v-if="init">%fa:spinner .pulse .fw%</div>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/mobile/tags/images.tag b/src/web/app/mobile/tags/images.tag
index f4a103311..7d95d6de2 100644
--- a/src/web/app/mobile/tags/images.tag
+++ b/src/web/app/mobile/tags/images.tag
@@ -1,7 +1,7 @@
 <mk-images>
-	<virtual each={ image in images }>
+	<template each={ image in images }>
 		<mk-images-image image={ image }/>
-	</virtual>
+	</template>
 	<style lang="stylus" scoped>
 		:scope
 			display grid
diff --git a/src/web/app/mobile/tags/init-following.tag b/src/web/app/mobile/tags/init-following.tag
index 94949a2e2..bf8313872 100644
--- a/src/web/app/mobile/tags/init-following.tag
+++ b/src/web/app/mobile/tags/init-following.tag
@@ -1,9 +1,9 @@
 <mk-init-following>
 	<p class="title">気になるユーザーをフォロー:</p>
 	<div class="users" v-if="!fetching && users.length > 0">
-		<virtual each={ users }>
+		<template each={ users }>
 			<mk-user-card user={ this } />
-		</virtual>
+		</template>
 	</div>
 	<p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p>
 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
diff --git a/src/web/app/mobile/tags/notification-preview.tag b/src/web/app/mobile/tags/notification-preview.tag
index bd4f633f8..bc37f198e 100644
--- a/src/web/app/mobile/tags/notification-preview.tag
+++ b/src/web/app/mobile/tags/notification-preview.tag
@@ -1,52 +1,52 @@
 <mk-notification-preview :class="{ notification.type }">
-	<virtual v-if="notification.type == 'reaction'">
+	<template v-if="notification.type == 'reaction'">
 		<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
 			<p><mk-reaction-icon reaction={ notification.reaction }/>{ notification.user.name }</p>
 			<p class="post-ref">%fa:quote-left%{ getPostSummary(notification.post) }%fa:quote-right%</p>
 		</div>
-	</virtual>
-	<virtual v-if="notification.type == 'repost'">
+	</template>
+	<template v-if="notification.type == 'repost'">
 		<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
 			<p>%fa:retweet%{ notification.post.user.name }</p>
 			<p class="post-ref">%fa:quote-left%{ getPostSummary(notification.post.repost) }%fa:quote-right%</p>
 		</div>
-	</virtual>
-	<virtual v-if="notification.type == 'quote'">
+	</template>
+	<template v-if="notification.type == 'quote'">
 		<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
 			<p>%fa:quote-left%{ notification.post.user.name }</p>
 			<p class="post-preview">{ getPostSummary(notification.post) }</p>
 		</div>
-	</virtual>
-	<virtual v-if="notification.type == 'follow'">
+	</template>
+	<template v-if="notification.type == 'follow'">
 		<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
 			<p>%fa:user-plus%{ notification.user.name }</p>
 		</div>
-	</virtual>
-	<virtual v-if="notification.type == 'reply'">
+	</template>
+	<template v-if="notification.type == 'reply'">
 		<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
 			<p>%fa:reply%{ notification.post.user.name }</p>
 			<p class="post-preview">{ getPostSummary(notification.post) }</p>
 		</div>
-	</virtual>
-	<virtual v-if="notification.type == 'mention'">
+	</template>
+	<template v-if="notification.type == 'mention'">
 		<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
 			<p>%fa:at%{ notification.post.user.name }</p>
 			<p class="post-preview">{ getPostSummary(notification.post) }</p>
 		</div>
-	</virtual>
-	<virtual v-if="notification.type == 'poll_vote'">
+	</template>
+	<template v-if="notification.type == 'poll_vote'">
 		<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		<div class="text">
 			<p>%fa:chart-pie%{ notification.user.name }</p>
 			<p class="post-ref">%fa:quote-left%{ getPostSummary(notification.post) }%fa:quote-right%</p>
 		</div>
-	</virtual>
+	</template>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/mobile/tags/notification.tag b/src/web/app/mobile/tags/notification.tag
index d4f6ca92e..c942e21aa 100644
--- a/src/web/app/mobile/tags/notification.tag
+++ b/src/web/app/mobile/tags/notification.tag
@@ -1,6 +1,6 @@
 <mk-notification :class="{ notification.type }">
 	<mk-time time={ notification.created_at }/>
-	<virtual v-if="notification.type == 'reaction'">
+	<template v-if="notification.type == 'reaction'">
 		<a class="avatar-anchor" href={ '/' + notification.user.username }>
 			<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		</a>
@@ -13,8 +13,8 @@
 				%fa:quote-left%{ getPostSummary(notification.post) }%fa:quote-right%
 			</a>
 		</div>
-	</virtual>
-	<virtual v-if="notification.type == 'repost'">
+	</template>
+	<template v-if="notification.type == 'repost'">
 		<a class="avatar-anchor" href={ '/' + notification.post.user.username }>
 			<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		</a>
@@ -27,8 +27,8 @@
 				%fa:quote-left%{ getPostSummary(notification.post.repost) }%fa:quote-right%
 			</a>
 		</div>
-	</virtual>
-	<virtual v-if="notification.type == 'quote'">
+	</template>
+	<template v-if="notification.type == 'quote'">
 		<a class="avatar-anchor" href={ '/' + notification.post.user.username }>
 			<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		</a>
@@ -39,8 +39,8 @@
 			</p>
 			<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
 		</div>
-	</virtual>
-	<virtual v-if="notification.type == 'follow'">
+	</template>
+	<template v-if="notification.type == 'follow'">
 		<a class="avatar-anchor" href={ '/' + notification.user.username }>
 			<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		</a>
@@ -50,8 +50,8 @@
 				<a href={ '/' + notification.user.username }>{ notification.user.name }</a>
 			</p>
 		</div>
-	</virtual>
-	<virtual v-if="notification.type == 'reply'">
+	</template>
+	<template v-if="notification.type == 'reply'">
 		<a class="avatar-anchor" href={ '/' + notification.post.user.username }>
 			<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		</a>
@@ -62,8 +62,8 @@
 			</p>
 			<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
 		</div>
-	</virtual>
-	<virtual v-if="notification.type == 'mention'">
+	</template>
+	<template v-if="notification.type == 'mention'">
 		<a class="avatar-anchor" href={ '/' + notification.post.user.username }>
 			<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		</a>
@@ -74,8 +74,8 @@
 			</p>
 			<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
 		</div>
-	</virtual>
-	<virtual v-if="notification.type == 'poll_vote'">
+	</template>
+	<template v-if="notification.type == 'poll_vote'">
 		<a class="avatar-anchor" href={ '/' + notification.user.username }>
 			<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
 		</a>
@@ -88,7 +88,7 @@
 				%fa:quote-left%{ getPostSummary(notification.post) }%fa:quote-right%
 			</a>
 		</div>
-	</virtual>
+	</template>
 	<style lang="stylus" scoped>
 		:scope
 			display block
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index 2ff961ae2..c945f6a3e 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -1,12 +1,12 @@
 <mk-notifications>
 	<div class="notifications" v-if="notifications.length != 0">
-		<virtual each={ notification, i in notifications }>
+		<template each={ notification, i in notifications }>
 			<mk-notification notification={ notification }/>
 			<p class="date" v-if="i != notifications.length - 1 && notification._date != notifications[i + 1]._date"><span>%fa:angle-up%{ notification._datetext }</span><span>%fa:angle-down%{ notifications[i + 1]._datetext }</span></p>
-		</virtual>
+		</template>
 	</div>
 	<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" disabled={ fetchingMoreNotifications }>
-		<virtual v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</virtual>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-notifications.more%' }
+		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-notifications.more%' }
 	</button>
 	<p class="empty" v-if="notifications.length == 0 && !loading">%i18n:mobile.tags.mk-notifications.empty%</p>
 	<p class="loading" v-if="loading">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index 124a707d2..d812aba42 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -1,12 +1,12 @@
 <mk-post-detail>
 	<button class="read-more" v-if="p.reply && p.reply.reply_id && context == null" @click="loadContext" disabled={ loadingContext }>
-		<virtual v-if="!contextFetching">%fa:ellipsis-v%</virtual>
-		<virtual v-if="contextFetching">%fa:spinner .pulse%</virtual>
+		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
+		<template v-if="contextFetching">%fa:spinner .pulse%</template>
 	</button>
 	<div class="context">
-		<virtual each={ post in context }>
+		<template each={ post in context }>
 			<mk-post-detail-sub post={ post }/>
-		</virtual>
+		</template>
 	</div>
 	<div class="reply-to" v-if="p.reply">
 		<mk-post-detail-sub post={ p.reply }/>
@@ -58,9 +58,9 @@
 		</footer>
 	</article>
 	<div class="replies" v-if="!compact">
-		<virtual each={ post in replies }>
+		<template each={ post in replies }>
 			<mk-post-detail-sub post={ post }/>
-		</virtual>
+		</template>
 	</div>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index b1ff03547..ed3f88c04 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -5,13 +5,13 @@
 	<div class="empty" v-if="!init && posts.length == 0">
 		%fa:R comments%{ opts.empty || '%i18n:mobile.tags.mk-timeline.empty%' }
 	</div>
-	<virtual each={ post, i in posts }>
+	<template each={ post, i in posts }>
 		<mk-timeline-post post={ post }/>
 		<p class="date" v-if="i != posts.length - 1 && post._date != posts[i + 1]._date">
 			<span>%fa:angle-up%{ post._datetext }</span>
 			<span>%fa:angle-down%{ posts[i + 1]._datetext }</span>
 		</p>
-	</virtual>
+	</template>
 	<footer v-if="!init">
 		<button v-if="canFetchMore" @click="more" disabled={ fetching }>
 			<span v-if="!fetching">%i18n:mobile.tags.mk-timeline.load-more%</span>
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index 16fb116eb..0a4483fd2 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -53,7 +53,7 @@
 		<div class="backdrop"></div>
 		<div class="content">
 			<button class="nav" @click="parent.toggleDrawer">%fa:bars%</button>
-			<virtual v-if="hasUnreadNotifications || hasUnreadMessagingMessages">%fa:circle%</virtual>
+			<template v-if="hasUnreadNotifications || hasUnreadMessagingMessages">%fa:circle%</template>
 			<h1 ref="title">Misskey</h1>
 			<button v-if="func" @click="func"><mk-raw content={ funcIcon }/></button>
 		</div>
@@ -234,8 +234,8 @@
 		<div class="links">
 			<ul>
 				<li><a href="/">%fa:home%%i18n:mobile.tags.mk-ui-nav.home%%fa:angle-right%</a></li>
-				<li><a href="/i/notifications">%fa:R bell%%i18n:mobile.tags.mk-ui-nav.notifications%<virtual v-if="hasUnreadNotifications">%fa:circle%</virtual>%fa:angle-right%</a></li>
-				<li><a href="/i/messaging">%fa:R comments%%i18n:mobile.tags.mk-ui-nav.messaging%<virtual v-if="hasUnreadMessagingMessages">%fa:circle%</virtual>%fa:angle-right%</a></li>
+				<li><a href="/i/notifications">%fa:R bell%%i18n:mobile.tags.mk-ui-nav.notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</a></li>
+				<li><a href="/i/messaging">%fa:R comments%%i18n:mobile.tags.mk-ui-nav.messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</a></li>
 			</ul>
 			<ul>
 				<li><a href={ _CH_URL_ } target="_blank">%fa:tv%%i18n:mobile.tags.mk-ui-nav.ch%%fa:angle-right%</a></li>
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index d0874f8e7..0091bafc2 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -309,9 +309,9 @@
 <mk-user-overview-posts>
 	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-posts.loading%<mk-ellipsis/></p>
 	<div v-if="!initializing && posts.length > 0">
-		<virtual each={ posts }>
+		<template each={ posts }>
 			<mk-user-overview-posts-post-card post={ this }/>
-		</virtual>
+		</template>
 	</div>
 	<p class="empty" v-if="!initializing && posts.length == 0">%i18n:mobile.tags.mk-user-overview-posts.no-posts%</p>
 	<style lang="stylus" scoped>
@@ -438,9 +438,9 @@
 <mk-user-overview-photos>
 	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-photos.loading%<mk-ellipsis/></p>
 	<div class="stream" v-if="!initializing && images.length > 0">
-		<virtual each={ image in images }>
+		<template each={ image in images }>
 			<a class="img" style={ 'background-image: url(' + image.media.url + '?thumbnail&size=256)' } href={ '/' + image.post.user.username + '/' + image.post.id }></a>
-		</virtual>
+		</template>
 	</div>
 	<p class="empty" v-if="!initializing && images.length == 0">%i18n:mobile.tags.mk-user-overview-photos.no-photos%</p>
 	<style lang="stylus" scoped>
@@ -559,9 +559,9 @@
 
 <mk-user-overview-keywords>
 	<div v-if="user.keywords != null && user.keywords.length > 1">
-		<virtual each={ keyword in user.keywords }>
+		<template each={ keyword in user.keywords }>
 			<a>{ keyword }</a>
-		</virtual>
+		</template>
 	</div>
 	<p class="empty" v-if="user.keywords == null || user.keywords.length == 0">%i18n:mobile.tags.mk-user-overview-keywords.no-keywords%</p>
 	<style lang="stylus" scoped>
@@ -593,9 +593,9 @@
 
 <mk-user-overview-domains>
 	<div v-if="user.domains != null && user.domains.length > 1">
-		<virtual each={ domain in user.domains }>
+		<template each={ domain in user.domains }>
 			<a style="opacity: { 0.5 + (domain.weight / 2) }">{ domain.domain }</a>
-		</virtual>
+		</template>
 	</div>
 	<p class="empty" v-if="user.domains == null || user.domains.length == 0">%i18n:mobile.tags.mk-user-overview-domains.no-domains%</p>
 	<style lang="stylus" scoped>
@@ -628,9 +628,9 @@
 <mk-user-overview-frequently-replied-users>
 	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-frequently-replied-users.loading%<mk-ellipsis/></p>
 	<div v-if="!initializing && users.length > 0">
-		<virtual each={ users }>
+		<template each={ users }>
 			<mk-user-card user={ this.user }/>
-		</virtual>
+		</template>
 	</div>
 	<p class="empty" v-if="!initializing && users.length == 0">%i18n:mobile.tags.mk-user-overview-frequently-replied-users.no-users%</p>
 	<style lang="stylus" scoped>
@@ -680,9 +680,9 @@
 <mk-user-overview-followers-you-know>
 	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p>
 	<div v-if="!initializing && users.length > 0">
-		<virtual each={ user in users }>
+		<template each={ user in users }>
 			<a href={ '/' + user.username }><img src={ user.avatar_url + '?thumbnail&size=64' } alt={ user.name }/></a>
-		</virtual>
+		</template>
 	</div>
 	<p class="empty" v-if="!initializing && users.length == 0">%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p>
 	<style lang="stylus" scoped>

From 941206922a523c6bee13e46d4ac268f633b324f9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Feb 2018 14:54:16 +0900
Subject: [PATCH 016/286] wip

---
 src/web/app/common/tags/poll.vue | 92 +++++++++++++++-----------------
 1 file changed, 44 insertions(+), 48 deletions(-)

diff --git a/src/web/app/common/tags/poll.vue b/src/web/app/common/tags/poll.vue
index 0b0132875..472a5f48c 100644
--- a/src/web/app/common/tags/poll.vue
+++ b/src/web/app/common/tags/poll.vue
@@ -1,19 +1,19 @@
 <template>
 <div :data-is-voted="isVoted">
 	<ul>
-		<li v-for="choice in poll.choices" :key="choice.id" @click="vote.bind(choice.id)" :class="{ voted: choice.voted }" :title="!choice.isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', choice.text) : ''">
+		<li v-for="choice in poll.choices" :key="choice.id" @click="vote.bind(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', choice.text) : ''">
 			<div class="backdrop" :style="{ 'width:' + (result ? (choice.votes / total * 100) : 0) + '%' }"></div>
 			<span>
-				<template v-if="is_voted">%fa:check%</template>
+				<template v-if="choice.is_voted">%fa:check%</template>
 				{{ text }}
-				<span class="votes" v-if="parent.result">({{ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', votes) }})</span>
+				<span class="votes" v-if="result">({{ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', choice.votes) }})</span>
 			</span>
 		</li>
 	</ul>
 	<p v-if="total > 0">
-		<span>{ '%i18n:common.tags.mk-poll.total-users%'.replace('{}', total) }</span>
+		<span>{{ '%i18n:common.tags.mk-poll.total-users%'.replace('{}', total) }}</span>
 		・
-		<a v-if="!isVoted" @click="toggleResult">{ result ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }</a>
+		<a v-if="!isVoted" @click="toggleResult">{{ result ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }}</a>
 		<span v-if="isVoted">%i18n:common.tags.mk-poll.voted%</span>
 	</p>
 </div>
@@ -59,59 +59,55 @@
 	};
 </script>
 
-<mk-poll data-is-voted={ isVoted }>
+<style lang="stylus" scoped>
+	:scope
+		display block
 
-	<style lang="stylus" scoped>
-		:scope
+		> ul
 			display block
+			margin 0
+			padding 0
+			list-style none
 
-			> ul
+			> li
 				display block
-				margin 0
-				padding 0
-				list-style none
+				margin 4px 0
+				padding 4px 8px
+				width 100%
+				border solid 1px #eee
+				border-radius 4px
+				overflow hidden
+				cursor pointer
 
-				> li
-					display block
-					margin 4px 0
-					padding 4px 8px
-					width 100%
-					border solid 1px #eee
-					border-radius 4px
-					overflow hidden
-					cursor pointer
+				&:hover
+					background rgba(0, 0, 0, 0.05)
 
-					&:hover
-						background rgba(0, 0, 0, 0.05)
+				&:active
+					background rgba(0, 0, 0, 0.1)
 
-					&:active
-						background rgba(0, 0, 0, 0.1)
+				> .backdrop
+					position absolute
+					top 0
+					left 0
+					height 100%
+					background $theme-color
+					transition width 1s ease
 
-					> .backdrop
-						position absolute
-						top 0
-						left 0
-						height 100%
-						background $theme-color
-						transition width 1s ease
+				> .votes
+					margin-left 4px
 
-					> .votes
-						margin-left 4px
+		> p
+			a
+				color inherit
 
-			> p
-				a
-					color inherit
+		&[data-is-voted]
+			> ul > li
+				cursor default
 
-			&[data-is-voted]
-				> ul > li
-					cursor default
+				&:hover
+					background transparent
 
-					&:hover
-						background transparent
+				&:active
+					background transparent
 
-					&:active
-						background transparent
-
-	</style>
-
-</mk-poll>
+</style>

From aa0dadcf0760541878c21eb3ab16aaf72063f33e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 8 Feb 2018 15:07:55 +0900
Subject: [PATCH 017/286] wip

---
 src/web/app/common/tags/poll.vue | 83 +++++++++++++++++---------------
 1 file changed, 44 insertions(+), 39 deletions(-)

diff --git a/src/web/app/common/tags/poll.vue b/src/web/app/common/tags/poll.vue
index 472a5f48c..d85caa00c 100644
--- a/src/web/app/common/tags/poll.vue
+++ b/src/web/app/common/tags/poll.vue
@@ -2,60 +2,65 @@
 <div :data-is-voted="isVoted">
 	<ul>
 		<li v-for="choice in poll.choices" :key="choice.id" @click="vote.bind(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', choice.text) : ''">
-			<div class="backdrop" :style="{ 'width:' + (result ? (choice.votes / total * 100) : 0) + '%' }"></div>
+			<div class="backdrop" :style="{ 'width:' + (showResult ? (choice.votes / total * 100) : 0) + '%' }"></div>
 			<span>
 				<template v-if="choice.is_voted">%fa:check%</template>
 				{{ text }}
-				<span class="votes" v-if="result">({{ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', choice.votes) }})</span>
+				<span class="votes" v-if="showResult">({{ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', choice.votes) }})</span>
 			</span>
 		</li>
 	</ul>
 	<p v-if="total > 0">
 		<span>{{ '%i18n:common.tags.mk-poll.total-users%'.replace('{}', total) }}</span>
 		・
-		<a v-if="!isVoted" @click="toggleResult">{{ result ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }}</a>
+		<a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? '%i18n:common.tags.mk-poll.vote%' : '%i18n:common.tags.mk-poll.show-result%' }}</a>
 		<span v-if="isVoted">%i18n:common.tags.mk-poll.voted%</span>
 	</p>
 </div>
 </template>
 
 <script lang="typescript">
-	this.mixin('api');
-
-	this.init = post => {
-		this.post = post;
-		this.poll = this.post.poll;
-		this.total = this.poll.choices.reduce((a, b) => a + b.votes, 0);
-		this.isVoted = this.poll.choices.some(c => c.is_voted);
-		this.result = this.isVoted;
-		this.update();
-	};
-
-	this.init(this.opts.post);
-
-	this.toggleResult = () => {
-		this.result = !this.result;
-	};
-
-	this.vote = id => {
-		if (this.poll.choices.some(c => c.is_voted)) return;
-		this.api('posts/polls/vote', {
-			post_id: this.post.id,
-			choice: id
-		}).then(() => {
-			this.poll.choices.forEach(c => {
-				if (c.id == id) {
-					c.votes++;
-					c.is_voted = true;
-				}
-			});
-			this.update({
-				poll: this.poll,
-				isVoted: true,
-				result: true,
-				total: this.total + 1
-			});
-		});
+	export default {
+		props: ['post'],
+		data() {
+			return {
+				showResult: false
+			};
+		},
+		computed: {
+			poll() {
+				return this.post.poll;
+			},
+			total() {
+				return this.poll.choices.reduce((a, b) => a + b.votes, 0);
+			},
+			isVoted() {
+				return this.poll.choices.some(c => c.is_voted);
+			}
+		},
+		created() {
+			this.showResult = this.isVoted;
+		},
+		methods: {
+			toggleShowResult() {
+				this.showResult = !this.showResult;
+			},
+			vote(id) {
+				if (this.poll.choices.some(c => c.is_voted)) return;
+				this.api('posts/polls/vote', {
+					post_id: this.post.id,
+					choice: id
+				}).then(() => {
+					this.poll.choices.forEach(c => {
+						if (c.id == id) {
+							c.votes++;
+							c.is_voted = true;
+						}
+					});
+					this.showResult = true;
+				});
+			}
+		}
 	};
 </script>
 

From d1aba96a2909654e9869edd18b43c5f863719e99 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 9 Feb 2018 09:46:23 +0900
Subject: [PATCH 018/286] wip

---
 src/web/app/desktop/tags/home.tag | 388 -----------------------------
 src/web/app/desktop/tags/home.vue | 392 ++++++++++++++++++++++++++++++
 2 files changed, 392 insertions(+), 388 deletions(-)
 delete mode 100644 src/web/app/desktop/tags/home.tag
 create mode 100644 src/web/app/desktop/tags/home.vue

diff --git a/src/web/app/desktop/tags/home.tag b/src/web/app/desktop/tags/home.tag
deleted file mode 100644
index 827622930..000000000
--- a/src/web/app/desktop/tags/home.tag
+++ /dev/null
@@ -1,388 +0,0 @@
-<mk-home data-customize={ opts.customize }>
-	<div class="customize" v-if="opts.customize">
-		<a href="/">%fa:check%完了</a>
-		<div>
-			<div class="adder">
-				<p>ウィジェットを追加:</p>
-				<select ref="widgetSelector">
-					<option value="profile">プロフィール</option>
-					<option value="calendar">カレンダー</option>
-					<option value="timemachine">カレンダー(タイムマシン)</option>
-					<option value="activity">アクティビティ</option>
-					<option value="rss-reader">RSSリーダー</option>
-					<option value="trends">トレンド</option>
-					<option value="photo-stream">フォトストリーム</option>
-					<option value="slideshow">スライドショー</option>
-					<option value="version">バージョン</option>
-					<option value="broadcast">ブロードキャスト</option>
-					<option value="notifications">通知</option>
-					<option value="user-recommendation">おすすめユーザー</option>
-					<option value="recommended-polls">投票</option>
-					<option value="post-form">投稿フォーム</option>
-					<option value="messaging">メッセージ</option>
-					<option value="channel">チャンネル</option>
-					<option value="access-log">アクセスログ</option>
-					<option value="server">サーバー情報</option>
-					<option value="donation">寄付のお願い</option>
-					<option value="nav">ナビゲーション</option>
-					<option value="tips">ヒント</option>
-				</select>
-				<button @click="addWidget">追加</button>
-			</div>
-			<div class="trash">
-				<div ref="trash"></div>
-				<p>ゴミ箱</p>
-			</div>
-		</div>
-	</div>
-	<div class="main">
-		<div class="left">
-			<div ref="left" data-place="left"></div>
-		</div>
-		<main ref="main">
-			<div class="maintop" ref="maintop" data-place="main" v-if="opts.customize"></div>
-			<mk-timeline-home-widget ref="tl" v-if="mode == 'timeline'"/>
-			<mk-mentions-home-widget ref="tl" v-if="mode == 'mentions'"/>
-		</main>
-		<div class="right">
-			<div ref="right" data-place="right"></div>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			&[data-customize]
-				padding-top 48px
-				background-image url('/assets/desktop/grid.svg')
-
-				> .main > main > *:not(.maintop)
-					cursor not-allowed
-
-					> *
-						pointer-events none
-
-			&:not([data-customize])
-				> .main > *:empty
-					display none
-
-			> .customize
-				position fixed
-				z-index 1000
-				top 0
-				left 0
-				width 100%
-				height 48px
-				background #f7f7f7
-				box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
-
-				> a
-					display block
-					position absolute
-					z-index 1001
-					top 0
-					right 0
-					padding 0 16px
-					line-height 48px
-					text-decoration none
-					color $theme-color-foreground
-					background $theme-color
-					transition background 0.1s ease
-
-					&:hover
-						background lighten($theme-color, 10%)
-
-					&:active
-						background darken($theme-color, 10%)
-						transition background 0s ease
-
-					> [data-fa]
-						margin-right 8px
-
-				> div
-					display flex
-					margin 0 auto
-					max-width 1200px - 32px
-
-					> div
-						width 50%
-
-						&.adder
-							> p
-								display inline
-								line-height 48px
-
-						&.trash
-							border-left solid 1px #ddd
-
-							> div
-								width 100%
-								height 100%
-
-							> p
-								position absolute
-								top 0
-								left 0
-								width 100%
-								line-height 48px
-								margin 0
-								text-align center
-								pointer-events none
-
-			> .main
-				display flex
-				justify-content center
-				margin 0 auto
-				max-width 1200px
-
-				> *
-					.customize-container
-						cursor move
-
-						> *
-							pointer-events none
-
-				> main
-					padding 16px
-					width calc(100% - 275px * 2)
-
-					> *:not(.maintop):not(:last-child)
-					> .maintop > *:not(:last-child)
-						margin-bottom 16px
-
-					> .maintop
-						min-height 64px
-						margin-bottom 16px
-
-				> *:not(main)
-					width 275px
-
-					> *
-						padding 16px 0 16px 0
-
-						> *:not(:last-child)
-							margin-bottom 16px
-
-				> .left
-					padding-left 16px
-
-				> .right
-					padding-right 16px
-
-				@media (max-width 1100px)
-					> *:not(main)
-						display none
-
-					> main
-						float none
-						width 100%
-						max-width 700px
-						margin 0 auto
-
-	</style>
-	<script lang="typescript">
-		import uuid from 'uuid';
-		import Sortable from 'sortablejs';
-		import dialog from '../scripts/dialog';
-		import ScrollFollower from '../scripts/scroll-follower';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.mode = this.opts.mode || 'timeline';
-
-		this.home = [];
-
-		this.bakeHomeData = () => JSON.stringify(this.I.client_settings.home);
-		this.bakedHomeData = this.bakeHomeData();
-
-		this.on('mount', () => {
-			this.$refs.tl.on('loaded', () => {
-				this.trigger('loaded');
-			});
-
-			this.I.on('refreshed', this.onMeRefreshed);
-
-			this.I.client_settings.home.forEach(widget => {
-				try {
-					this.setWidget(widget);
-				} catch (e) {
-					// noop
-				}
-			});
-
-			if (!this.opts.customize) {
-				if (this.$refs.left.children.length == 0) {
-					this.$refs.left.parentNode.removeChild(this.$refs.left);
-				}
-				if (this.$refs.right.children.length == 0) {
-					this.$refs.right.parentNode.removeChild(this.$refs.right);
-				}
-			}
-
-			if (this.opts.customize) {
-				dialog('%fa:info-circle%カスタマイズのヒント',
-					'<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' +
-					'<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' +
-					'<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' +
-					'<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>',
-				[{
-					text: 'Got it!'
-				}]);
-
-				const sortableOption = {
-					group: 'kyoppie',
-					animation: 150,
-					onMove: evt => {
-						const id = evt.dragged.getAttribute('data-widget-id');
-						this.home.find(tag => tag.id == id).update({ place: evt.to.getAttribute('data-place') });
-					},
-					onSort: () => {
-						this.saveHome();
-					}
-				};
-
-				new Sortable(this.$refs.left, sortableOption);
-				new Sortable(this.$refs.right, sortableOption);
-				new Sortable(this.$refs.maintop, sortableOption);
-				new Sortable(this.$refs.trash, Object.assign({}, sortableOption, {
-					onAdd: evt => {
-						const el = evt.item;
-						const id = el.getAttribute('data-widget-id');
-						el.parentNode.removeChild(el);
-						this.I.client_settings.home = this.I.client_settings.home.filter(w => w.id != id);
-						this.saveHome();
-					}
-				}));
-			}
-
-			if (!this.opts.customize) {
-				this.scrollFollowerLeft = this.$refs.left.parentNode ? new ScrollFollower(this.$refs.left, this.root.getBoundingClientRect().top) : null;
-				this.scrollFollowerRight = this.$refs.right.parentNode ? new ScrollFollower(this.$refs.right, this.root.getBoundingClientRect().top) : null;
-			}
-		});
-
-		this.on('unmount', () => {
-			this.I.off('refreshed', this.onMeRefreshed);
-
-			this.home.forEach(widget => {
-				widget.unmount();
-			});
-
-			if (!this.opts.customize) {
-				if (this.scrollFollowerLeft) this.scrollFollowerLeft.dispose();
-				if (this.scrollFollowerRight) this.scrollFollowerRight.dispose();
-			}
-		});
-
-		this.onMeRefreshed = () => {
-			if (this.bakedHomeData != this.bakeHomeData()) {
-				alert('別の場所でホームが編集されました。ページを再度読み込みすると編集が反映されます。');
-			}
-		};
-
-		this.setWidget = (widget, prepend = false) => {
-			const el = document.createElement(`mk-${widget.name}-home-widget`);
-
-			let actualEl;
-
-			if (this.opts.customize) {
-				const container = document.createElement('div');
-				container.classList.add('customize-container');
-				container.setAttribute('data-widget-id', widget.id);
-				container.appendChild(el);
-				actualEl = container;
-			} else {
-				actualEl = el;
-			}
-
-			switch (widget.place) {
-				case 'left':
-					if (prepend) {
-						this.$refs.left.insertBefore(actualEl, this.$refs.left.firstChild);
-					} else {
-						this.$refs.left.appendChild(actualEl);
-					}
-					break;
-				case 'right':
-					if (prepend) {
-						this.$refs.right.insertBefore(actualEl, this.$refs.right.firstChild);
-					} else {
-						this.$refs.right.appendChild(actualEl);
-					}
-					break;
-				case 'main':
-					if (this.opts.customize) {
-						this.$refs.maintop.appendChild(actualEl);
-					} else {
-						this.$refs.main.insertBefore(actualEl, this.$refs.tl.root);
-					}
-					break;
-			}
-
-			const tag = riot.mount(el, {
-				id: widget.id,
-				data: widget.data,
-				place: widget.place,
-				tl: this.$refs.tl
-			})[0];
-
-			this.home.push(tag);
-
-			if (this.opts.customize) {
-				actualEl.oncontextmenu = e => {
-					e.preventDefault();
-					e.stopImmediatePropagation();
-					if (tag.func) tag.func();
-					return false;
-				};
-			}
-		};
-
-		this.addWidget = () => {
-			const widget = {
-				name: this.$refs.widgetSelector.options[this.$refs.widgetSelector.selectedIndex].value,
-				id: uuid(),
-				place: 'left',
-				data: {}
-			};
-
-			this.I.client_settings.home.unshift(widget);
-
-			this.setWidget(widget, true);
-
-			this.saveHome();
-		};
-
-		this.saveHome = () => {
-			const data = [];
-
-			Array.from(this.$refs.left.children).forEach(el => {
-				const id = el.getAttribute('data-widget-id');
-				const widget = this.I.client_settings.home.find(w => w.id == id);
-				widget.place = 'left';
-				data.push(widget);
-			});
-
-			Array.from(this.$refs.right.children).forEach(el => {
-				const id = el.getAttribute('data-widget-id');
-				const widget = this.I.client_settings.home.find(w => w.id == id);
-				widget.place = 'right';
-				data.push(widget);
-			});
-
-			Array.from(this.$refs.maintop.children).forEach(el => {
-				const id = el.getAttribute('data-widget-id');
-				const widget = this.I.client_settings.home.find(w => w.id == id);
-				widget.place = 'main';
-				data.push(widget);
-			});
-
-			this.api('i/update_home', {
-				home: data
-			}).then(() => {
-				this.I.update();
-			});
-		};
-	</script>
-</mk-home>
diff --git a/src/web/app/desktop/tags/home.vue b/src/web/app/desktop/tags/home.vue
new file mode 100644
index 000000000..ee12200ba
--- /dev/null
+++ b/src/web/app/desktop/tags/home.vue
@@ -0,0 +1,392 @@
+<template>
+	<div :data-customize="customize">
+		<div class="customize" v-if="customize">
+			<a href="/">%fa:check%完了</a>
+			<div>
+				<div class="adder">
+					<p>ウィジェットを追加:</p>
+					<select ref="widgetSelector">
+						<option value="profile">プロフィール</option>
+						<option value="calendar">カレンダー</option>
+						<option value="timemachine">カレンダー(タイムマシン)</option>
+						<option value="activity">アクティビティ</option>
+						<option value="rss-reader">RSSリーダー</option>
+						<option value="trends">トレンド</option>
+						<option value="photo-stream">フォトストリーム</option>
+						<option value="slideshow">スライドショー</option>
+						<option value="version">バージョン</option>
+						<option value="broadcast">ブロードキャスト</option>
+						<option value="notifications">通知</option>
+						<option value="user-recommendation">おすすめユーザー</option>
+						<option value="recommended-polls">投票</option>
+						<option value="post-form">投稿フォーム</option>
+						<option value="messaging">メッセージ</option>
+						<option value="channel">チャンネル</option>
+						<option value="access-log">アクセスログ</option>
+						<option value="server">サーバー情報</option>
+						<option value="donation">寄付のお願い</option>
+						<option value="nav">ナビゲーション</option>
+						<option value="tips">ヒント</option>
+					</select>
+					<button @click="addWidget">追加</button>
+				</div>
+				<div class="trash">
+					<div ref="trash"></div>
+					<p>ゴミ箱</p>
+				</div>
+			</div>
+		</div>
+		<div class="main">
+			<div class="left">
+				<div ref="left" data-place="left"></div>
+			</div>
+			<main ref="main">
+				<div class="maintop" ref="maintop" data-place="main" v-if="customize"></div>
+				<mk-timeline-home-widget ref="tl" v-if="mode == 'timeline'"/>
+				<mk-mentions-home-widget ref="tl" v-if="mode == 'mentions'"/>
+			</main>
+			<div class="right">
+				<div ref="right" data-place="right"></div>
+			</div>
+		</div>	
+	</div>
+</template>
+
+<style lang="stylus" scoped>
+	:scope
+		display block
+
+		&[data-customize]
+			padding-top 48px
+			background-image url('/assets/desktop/grid.svg')
+
+			> .main > main > *:not(.maintop)
+				cursor not-allowed
+
+				> *
+					pointer-events none
+
+		&:not([data-customize])
+			> .main > *:empty
+				display none
+
+		> .customize
+			position fixed
+			z-index 1000
+			top 0
+			left 0
+			width 100%
+			height 48px
+			background #f7f7f7
+			box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
+
+			> a
+				display block
+				position absolute
+				z-index 1001
+				top 0
+				right 0
+				padding 0 16px
+				line-height 48px
+				text-decoration none
+				color $theme-color-foreground
+				background $theme-color
+				transition background 0.1s ease
+
+				&:hover
+					background lighten($theme-color, 10%)
+
+				&:active
+					background darken($theme-color, 10%)
+					transition background 0s ease
+
+				> [data-fa]
+					margin-right 8px
+
+			> div
+				display flex
+				margin 0 auto
+				max-width 1200px - 32px
+
+				> div
+					width 50%
+
+					&.adder
+						> p
+							display inline
+							line-height 48px
+
+					&.trash
+						border-left solid 1px #ddd
+
+						> div
+							width 100%
+							height 100%
+
+						> p
+							position absolute
+							top 0
+							left 0
+							width 100%
+							line-height 48px
+							margin 0
+							text-align center
+							pointer-events none
+
+		> .main
+			display flex
+			justify-content center
+			margin 0 auto
+			max-width 1200px
+
+			> *
+				.customize-container
+					cursor move
+
+					> *
+						pointer-events none
+
+			> main
+				padding 16px
+				width calc(100% - 275px * 2)
+
+				> *:not(.maintop):not(:last-child)
+				> .maintop > *:not(:last-child)
+					margin-bottom 16px
+
+				> .maintop
+					min-height 64px
+					margin-bottom 16px
+
+			> *:not(main)
+				width 275px
+
+				> *
+					padding 16px 0 16px 0
+
+					> *:not(:last-child)
+						margin-bottom 16px
+
+			> .left
+				padding-left 16px
+
+			> .right
+				padding-right 16px
+
+			@media (max-width 1100px)
+				> *:not(main)
+					display none
+
+				> main
+					float none
+					width 100%
+					max-width 700px
+					margin 0 auto
+
+</style>
+
+<script lang="typescript">
+	import uuid from 'uuid';
+	import Sortable from 'sortablejs';
+	import dialog from '../scripts/dialog';
+	import ScrollFollower from '../scripts/scroll-follower';
+
+	this.mixin('i');
+	this.mixin('api');
+
+	this.mode = this.opts.mode || 'timeline';
+
+	this.home = [];
+
+	this.bakeHomeData = () => JSON.stringify(this.I.client_settings.home);
+	this.bakedHomeData = this.bakeHomeData();
+
+	this.on('mount', () => {
+		this.$refs.tl.on('loaded', () => {
+			this.trigger('loaded');
+		});
+
+		this.I.on('refreshed', this.onMeRefreshed);
+
+		this.I.client_settings.home.forEach(widget => {
+			try {
+				this.setWidget(widget);
+			} catch (e) {
+				// noop
+			}
+		});
+
+		if (!this.opts.customize) {
+			if (this.$refs.left.children.length == 0) {
+				this.$refs.left.parentNode.removeChild(this.$refs.left);
+			}
+			if (this.$refs.right.children.length == 0) {
+				this.$refs.right.parentNode.removeChild(this.$refs.right);
+			}
+		}
+
+		if (this.opts.customize) {
+			dialog('%fa:info-circle%カスタマイズのヒント',
+				'<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' +
+				'<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' +
+				'<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' +
+				'<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>',
+			[{
+				text: 'Got it!'
+			}]);
+
+			const sortableOption = {
+				group: 'kyoppie',
+				animation: 150,
+				onMove: evt => {
+					const id = evt.dragged.getAttribute('data-widget-id');
+					this.home.find(tag => tag.id == id).update({ place: evt.to.getAttribute('data-place') });
+				},
+				onSort: () => {
+					this.saveHome();
+				}
+			};
+
+			new Sortable(this.$refs.left, sortableOption);
+			new Sortable(this.$refs.right, sortableOption);
+			new Sortable(this.$refs.maintop, sortableOption);
+			new Sortable(this.$refs.trash, Object.assign({}, sortableOption, {
+				onAdd: evt => {
+					const el = evt.item;
+					const id = el.getAttribute('data-widget-id');
+					el.parentNode.removeChild(el);
+					this.I.client_settings.home = this.I.client_settings.home.filter(w => w.id != id);
+					this.saveHome();
+				}
+			}));
+		}
+
+		if (!this.opts.customize) {
+			this.scrollFollowerLeft = this.$refs.left.parentNode ? new ScrollFollower(this.$refs.left, this.root.getBoundingClientRect().top) : null;
+			this.scrollFollowerRight = this.$refs.right.parentNode ? new ScrollFollower(this.$refs.right, this.root.getBoundingClientRect().top) : null;
+		}
+	});
+
+	this.on('unmount', () => {
+		this.I.off('refreshed', this.onMeRefreshed);
+
+		this.home.forEach(widget => {
+			widget.unmount();
+		});
+
+		if (!this.opts.customize) {
+			if (this.scrollFollowerLeft) this.scrollFollowerLeft.dispose();
+			if (this.scrollFollowerRight) this.scrollFollowerRight.dispose();
+		}
+	});
+
+	this.onMeRefreshed = () => {
+		if (this.bakedHomeData != this.bakeHomeData()) {
+			alert('別の場所でホームが編集されました。ページを再度読み込みすると編集が反映されます。');
+		}
+	};
+
+	this.setWidget = (widget, prepend = false) => {
+		const el = document.createElement(`mk-${widget.name}-home-widget`);
+
+		let actualEl;
+
+		if (this.opts.customize) {
+			const container = document.createElement('div');
+			container.classList.add('customize-container');
+			container.setAttribute('data-widget-id', widget.id);
+			container.appendChild(el);
+			actualEl = container;
+		} else {
+			actualEl = el;
+		}
+
+		switch (widget.place) {
+			case 'left':
+				if (prepend) {
+					this.$refs.left.insertBefore(actualEl, this.$refs.left.firstChild);
+				} else {
+					this.$refs.left.appendChild(actualEl);
+				}
+				break;
+			case 'right':
+				if (prepend) {
+					this.$refs.right.insertBefore(actualEl, this.$refs.right.firstChild);
+				} else {
+					this.$refs.right.appendChild(actualEl);
+				}
+				break;
+			case 'main':
+				if (this.opts.customize) {
+					this.$refs.maintop.appendChild(actualEl);
+				} else {
+					this.$refs.main.insertBefore(actualEl, this.$refs.tl.root);
+				}
+				break;
+		}
+
+		const tag = riot.mount(el, {
+			id: widget.id,
+			data: widget.data,
+			place: widget.place,
+			tl: this.$refs.tl
+		})[0];
+
+		this.home.push(tag);
+
+		if (this.opts.customize) {
+			actualEl.oncontextmenu = e => {
+				e.preventDefault();
+				e.stopImmediatePropagation();
+				if (tag.func) tag.func();
+				return false;
+			};
+		}
+	};
+
+	this.addWidget = () => {
+		const widget = {
+			name: this.$refs.widgetSelector.options[this.$refs.widgetSelector.selectedIndex].value,
+			id: uuid(),
+			place: 'left',
+			data: {}
+		};
+
+		this.I.client_settings.home.unshift(widget);
+
+		this.setWidget(widget, true);
+
+		this.saveHome();
+	};
+
+	this.saveHome = () => {
+		const data = [];
+
+		Array.from(this.$refs.left.children).forEach(el => {
+			const id = el.getAttribute('data-widget-id');
+			const widget = this.I.client_settings.home.find(w => w.id == id);
+			widget.place = 'left';
+			data.push(widget);
+		});
+
+		Array.from(this.$refs.right.children).forEach(el => {
+			const id = el.getAttribute('data-widget-id');
+			const widget = this.I.client_settings.home.find(w => w.id == id);
+			widget.place = 'right';
+			data.push(widget);
+		});
+
+		Array.from(this.$refs.maintop.children).forEach(el => {
+			const id = el.getAttribute('data-widget-id');
+			const widget = this.I.client_settings.home.find(w => w.id == id);
+			widget.place = 'main';
+			data.push(widget);
+		});
+
+		this.api('i/update_home', {
+			home: data
+		}).then(() => {
+			this.I.update();
+		});
+	};
+</script>

From 92a1429de9763a39541b065ddd86c8720f20500c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Feb 2018 13:11:30 +0900
Subject: [PATCH 019/286] wip

---
 src/web/app/auth/tags/form.tag                |   4 +-
 src/web/app/common/tags/messaging/index.tag   |   4 +-
 src/web/app/common/tags/signin.tag            |   2 +-
 src/web/app/common/tags/uploader.tag          |   6 +-
 src/web/app/desktop/tags/contextmenu.tag      |   2 +-
 src/web/app/desktop/tags/crop-window.tag      |   6 +-
 .../desktop/tags/drive/base-contextmenu.tag   |   2 +-
 src/web/app/desktop/tags/drive/browser.tag    |  10 +-
 .../desktop/tags/drive/file-contextmenu.tag   |   2 +-
 .../desktop/tags/drive/folder-contextmenu.tag |   2 +-
 .../desktop/tags/home-widgets/mentions.tag    |   2 +-
 .../desktop/tags/home-widgets/timeline.tag    |   2 +-
 src/web/app/desktop/tags/home.vue             | 488 +++++++++---------
 src/web/app/desktop/tags/post-form.tag        |  12 +-
 src/web/app/desktop/tags/repost-form.tag      |   6 +-
 src/web/app/desktop/tags/search-posts.tag     |   2 +-
 src/web/app/desktop/tags/search.tag           |   2 +-
 .../tags/select-file-from-drive-window.tag    |   2 +-
 .../tags/select-folder-from-drive-window.tag  |   2 +-
 src/web/app/desktop/tags/user-timeline.tag    |   2 +-
 src/web/app/desktop/tags/user.tag             |   6 +-
 src/web/app/desktop/tags/users-list.tag       |   2 +-
 src/web/app/desktop/tags/widgets/activity.tag |   2 +-
 src/web/app/desktop/tags/window.tag           |   8 +-
 .../app/mobile/tags/drive-folder-selector.tag |   4 +-
 src/web/app/mobile/tags/drive-selector.tag    |   6 +-
 src/web/app/mobile/tags/drive.tag             |  16 +-
 src/web/app/mobile/tags/home-timeline.tag     |   2 +-
 src/web/app/mobile/tags/home.tag              |   2 +-
 src/web/app/mobile/tags/notifications.tag     |   2 +-
 src/web/app/mobile/tags/post-form.tag         |  12 +-
 src/web/app/mobile/tags/search-posts.tag      |   2 +-
 src/web/app/mobile/tags/search.tag            |   2 +-
 src/web/app/mobile/tags/user-followers.tag    |   2 +-
 src/web/app/mobile/tags/user-following.tag    |   2 +-
 src/web/app/mobile/tags/user-timeline.tag     |   2 +-
 src/web/app/mobile/tags/user.tag              |   2 +-
 src/web/app/mobile/tags/users-list.tag        |   2 +-
 38 files changed, 308 insertions(+), 328 deletions(-)

diff --git a/src/web/app/auth/tags/form.tag b/src/web/app/auth/tags/form.tag
index f20165977..043b6313b 100644
--- a/src/web/app/auth/tags/form.tag
+++ b/src/web/app/auth/tags/form.tag
@@ -115,7 +115,7 @@
 			this.api('auth/deny', {
 				token: this.session.token
 			}).then(() => {
-				this.trigger('denied');
+				this.$emit('denied');
 			});
 		};
 
@@ -123,7 +123,7 @@
 			this.api('auth/accept', {
 				token: this.session.token
 			}).then(() => {
-				this.trigger('accepted');
+				this.$emit('accepted');
 			});
 		};
 	</script>
diff --git a/src/web/app/common/tags/messaging/index.tag b/src/web/app/common/tags/messaging/index.tag
index f7af153c2..0432f7e30 100644
--- a/src/web/app/common/tags/messaging/index.tag
+++ b/src/web/app/common/tags/messaging/index.tag
@@ -344,7 +344,7 @@
 		this.registerMessage = message => {
 			message.is_me = message.user_id == this.I.id;
 			message._click = () => {
-				this.trigger('navigate-user', message.is_me ? message.recipient : message.user);
+				this.$emit('navigate-user', message.is_me ? message.recipient : message.user);
 			};
 		};
 
@@ -400,7 +400,7 @@
 			}).then(users => {
 				users.forEach(user => {
 					user._click = () => {
-						this.trigger('navigate-user', user);
+						this.$emit('navigate-user', user);
 						this.searchResult = [];
 					};
 				});
diff --git a/src/web/app/common/tags/signin.tag b/src/web/app/common/tags/signin.tag
index 76a55c7e0..89213d1f7 100644
--- a/src/web/app/common/tags/signin.tag
+++ b/src/web/app/common/tags/signin.tag
@@ -111,7 +111,7 @@
 				username: this.$refs.username.value
 			}).then(user => {
 				this.user = user;
-				this.trigger('user', user);
+				this.$emit('user', user);
 				this.update();
 			});
 		};
diff --git a/src/web/app/common/tags/uploader.tag b/src/web/app/common/tags/uploader.tag
index 1dbfff96f..519b063fa 100644
--- a/src/web/app/common/tags/uploader.tag
+++ b/src/web/app/common/tags/uploader.tag
@@ -155,7 +155,7 @@
 			};
 
 			this.uploads.push(ctx);
-			this.trigger('change-uploads', this.uploads);
+			this.$emit('change-uploads', this.uploads);
 			this.update();
 
 			const reader = new FileReader();
@@ -176,10 +176,10 @@
 			xhr.onload = e => {
 				const driveFile = JSON.parse(e.target.response);
 
-				this.trigger('uploaded', driveFile);
+				this.$emit('uploaded', driveFile);
 
 				this.uploads = this.uploads.filter(x => x.id != id);
-				this.trigger('change-uploads', this.uploads);
+				this.$emit('change-uploads', this.uploads);
 
 				this.update();
 			};
diff --git a/src/web/app/desktop/tags/contextmenu.tag b/src/web/app/desktop/tags/contextmenu.tag
index 67bdc5824..ee4c48fbd 100644
--- a/src/web/app/desktop/tags/contextmenu.tag
+++ b/src/web/app/desktop/tags/contextmenu.tag
@@ -131,7 +131,7 @@
 				el.removeEventListener('mousedown', this.mousedown);
 			});
 
-			this.trigger('closed');
+			this.$emit('closed');
 			this.$destroy();
 		};
 	</script>
diff --git a/src/web/app/desktop/tags/crop-window.tag b/src/web/app/desktop/tags/crop-window.tag
index 1749986b2..c26f74b12 100644
--- a/src/web/app/desktop/tags/crop-window.tag
+++ b/src/web/app/desktop/tags/crop-window.tag
@@ -178,18 +178,18 @@
 
 		this.ok = () => {
 			this.cropper.getCroppedCanvas().toBlob(blob => {
-				this.trigger('cropped', blob);
+				this.$emit('cropped', blob);
 				this.$refs.window.close();
 			});
 		};
 
 		this.skip = () => {
-			this.trigger('skipped');
+			this.$emit('skipped');
 			this.$refs.window.close();
 		};
 
 		this.cancel = () => {
-			this.trigger('canceled');
+			this.$emit('canceled');
 			this.$refs.window.close();
 		};
 	</script>
diff --git a/src/web/app/desktop/tags/drive/base-contextmenu.tag b/src/web/app/desktop/tags/drive/base-contextmenu.tag
index f81526bef..c93d63026 100644
--- a/src/web/app/desktop/tags/drive/base-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/base-contextmenu.tag
@@ -17,7 +17,7 @@
 
 		this.on('mount', () => {
 			this.$refs.ctx.on('closed', () => {
-				this.trigger('closed');
+				this.$emit('closed');
 				this.$destroy();
 			});
 		});
diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/tags/drive/browser.tag
index 02d79afd8..7aaedab82 100644
--- a/src/web/app/desktop/tags/drive/browser.tag
+++ b/src/web/app/desktop/tags/drive/browser.tag
@@ -535,13 +535,13 @@
 					this.selectedFiles.push(file);
 				}
 				this.update();
-				this.trigger('change-selection', this.selectedFiles);
+				this.$emit('change-selection', this.selectedFiles);
 			} else {
 				if (isAlreadySelected) {
-					this.trigger('selected', file);
+					this.$emit('selected', file);
 				} else {
 					this.selectedFiles = [file];
-					this.trigger('change-selection', [file]);
+					this.$emit('change-selection', [file]);
 				}
 			}
 		};
@@ -578,7 +578,7 @@
 				if (folder.parent) dive(folder.parent);
 
 				this.update();
-				this.trigger('open-folder', folder);
+				this.$emit('open-folder', folder);
 				this.fetch();
 			});
 		};
@@ -648,7 +648,7 @@
 				folder: null,
 				hierarchyFolders: []
 			});
-			this.trigger('move-root');
+			this.$emit('move-root');
 			this.fetch();
 		};
 
diff --git a/src/web/app/desktop/tags/drive/file-contextmenu.tag b/src/web/app/desktop/tags/drive/file-contextmenu.tag
index c7eeb01cd..125f70b61 100644
--- a/src/web/app/desktop/tags/drive/file-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/file-contextmenu.tag
@@ -49,7 +49,7 @@
 
 		this.on('mount', () => {
 			this.$refs.ctx.on('closed', () => {
-				this.trigger('closed');
+				this.$emit('closed');
 				this.$destroy();
 			});
 		});
diff --git a/src/web/app/desktop/tags/drive/folder-contextmenu.tag b/src/web/app/desktop/tags/drive/folder-contextmenu.tag
index d4c2f9380..0cb7f6eb8 100644
--- a/src/web/app/desktop/tags/drive/folder-contextmenu.tag
+++ b/src/web/app/desktop/tags/drive/folder-contextmenu.tag
@@ -29,7 +29,7 @@
 			this.$refs.ctx.open(pos);
 
 			this.$refs.ctx.on('closed', () => {
-				this.trigger('closed');
+				this.$emit('closed');
 				this.$destroy();
 			});
 		};
diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/tags/home-widgets/mentions.tag
index e0592aa04..81f9b2875 100644
--- a/src/web/app/desktop/tags/home-widgets/mentions.tag
+++ b/src/web/app/desktop/tags/home-widgets/mentions.tag
@@ -65,7 +65,7 @@
 			document.addEventListener('keydown', this.onDocumentKeydown);
 			window.addEventListener('scroll', this.onScroll);
 
-			this.fetch(() => this.trigger('loaded'));
+			this.fetch(() => this.$emit('loaded'));
 		});
 
 		this.on('unmount', () => {
diff --git a/src/web/app/desktop/tags/home-widgets/timeline.tag b/src/web/app/desktop/tags/home-widgets/timeline.tag
index ac2d95d5a..4668ebfa8 100644
--- a/src/web/app/desktop/tags/home-widgets/timeline.tag
+++ b/src/web/app/desktop/tags/home-widgets/timeline.tag
@@ -59,7 +59,7 @@
 			document.addEventListener('keydown', this.onDocumentKeydown);
 			window.addEventListener('scroll', this.onScroll);
 
-			this.load(() => this.trigger('loaded'));
+			this.load(() => this.$emit('loaded'));
 		});
 
 		this.on('unmount', () => {
diff --git a/src/web/app/desktop/tags/home.vue b/src/web/app/desktop/tags/home.vue
index ee12200ba..981123c56 100644
--- a/src/web/app/desktop/tags/home.vue
+++ b/src/web/app/desktop/tags/home.vue
@@ -1,57 +1,243 @@
 <template>
-	<div :data-customize="customize">
-		<div class="customize" v-if="customize">
-			<a href="/">%fa:check%完了</a>
-			<div>
-				<div class="adder">
-					<p>ウィジェットを追加:</p>
-					<select ref="widgetSelector">
-						<option value="profile">プロフィール</option>
-						<option value="calendar">カレンダー</option>
-						<option value="timemachine">カレンダー(タイムマシン)</option>
-						<option value="activity">アクティビティ</option>
-						<option value="rss-reader">RSSリーダー</option>
-						<option value="trends">トレンド</option>
-						<option value="photo-stream">フォトストリーム</option>
-						<option value="slideshow">スライドショー</option>
-						<option value="version">バージョン</option>
-						<option value="broadcast">ブロードキャスト</option>
-						<option value="notifications">通知</option>
-						<option value="user-recommendation">おすすめユーザー</option>
-						<option value="recommended-polls">投票</option>
-						<option value="post-form">投稿フォーム</option>
-						<option value="messaging">メッセージ</option>
-						<option value="channel">チャンネル</option>
-						<option value="access-log">アクセスログ</option>
-						<option value="server">サーバー情報</option>
-						<option value="donation">寄付のお願い</option>
-						<option value="nav">ナビゲーション</option>
-						<option value="tips">ヒント</option>
-					</select>
-					<button @click="addWidget">追加</button>
-				</div>
-				<div class="trash">
-					<div ref="trash"></div>
-					<p>ゴミ箱</p>
-				</div>
+<div :data-customize="customize">
+	<div class="customize" v-if="customize">
+		<a href="/">%fa:check%完了</a>
+		<div>
+			<div class="adder">
+				<p>ウィジェットを追加:</p>
+				<select ref="widgetSelector">
+					<option value="profile">プロフィール</option>
+					<option value="calendar">カレンダー</option>
+					<option value="timemachine">カレンダー(タイムマシン)</option>
+					<option value="activity">アクティビティ</option>
+					<option value="rss-reader">RSSリーダー</option>
+					<option value="trends">トレンド</option>
+					<option value="photo-stream">フォトストリーム</option>
+					<option value="slideshow">スライドショー</option>
+					<option value="version">バージョン</option>
+					<option value="broadcast">ブロードキャスト</option>
+					<option value="notifications">通知</option>
+					<option value="user-recommendation">おすすめユーザー</option>
+					<option value="recommended-polls">投票</option>
+					<option value="post-form">投稿フォーム</option>
+					<option value="messaging">メッセージ</option>
+					<option value="channel">チャンネル</option>
+					<option value="access-log">アクセスログ</option>
+					<option value="server">サーバー情報</option>
+					<option value="donation">寄付のお願い</option>
+					<option value="nav">ナビゲーション</option>
+					<option value="tips">ヒント</option>
+				</select>
+				<button @click="addWidget">追加</button>
+			</div>
+			<div class="trash">
+				<div ref="trash"></div>
+				<p>ゴミ箱</p>
 			</div>
 		</div>
-		<div class="main">
-			<div class="left">
-				<div ref="left" data-place="left"></div>
-			</div>
-			<main ref="main">
-				<div class="maintop" ref="maintop" data-place="main" v-if="customize"></div>
-				<mk-timeline-home-widget ref="tl" v-if="mode == 'timeline'"/>
-				<mk-mentions-home-widget ref="tl" v-if="mode == 'mentions'"/>
-			</main>
-			<div class="right">
-				<div ref="right" data-place="right"></div>
-			</div>
-		</div>	
 	</div>
+	<div class="main">
+		<div class="left">
+			<div ref="left" data-place="left">
+				<template v-for="widget in leftWidgets">
+					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu="onWidgetContextmenu.stop.prevent(widget.id)">
+						<component :is="widget.name" :widget="widget" :ref="widget.id"></component>
+					</div>
+					<template v-else>
+						<component :is="widget.name" :key="widget.id" :widget="widget" :ref="widget.id"></component>
+					</template>
+				</template>
+			</div>
+		</div>
+		<main ref="main">
+			<div class="maintop" ref="maintop" data-place="main" v-if="customize">
+				<template v-for="widget in centerWidgets">
+					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu="onWidgetContextmenu.stop.prevent(widget.id)">
+						<component :is="widget.name" :widget="widget" :ref="widget.id"></component>
+					</div>
+					<template v-else>
+						<component :is="widget.name" :key="widget.id" :widget="widget" :ref="widget.id"></component>
+					</template>
+				</template>
+			</div>
+			<mk-timeline-home-widget ref="tl" v-on:loaded="onTlLoaded" v-if="mode == 'timeline'"/>
+			<mk-mentions-home-widget ref="tl" v-on:loaded="onTlLoaded" v-if="mode == 'mentions'"/>
+		</main>
+		<div class="right">
+			<div ref="right" data-place="right">
+				<template v-for="widget in rightWidgets">
+					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu="onWidgetContextmenu.stop.prevent(widget.id)">
+						<component :is="widget.name" :widget="widget" :ref="widget.id"></component>
+					</div>
+					<template v-else>
+						<component :is="widget.name" :key="widget.id" :widget="widget" :ref="widget.id"></component>
+					</template>
+				</template>
+			</div>
+		</div>
+	</div>
+</div>
 </template>
 
+<script lang="typescript">
+import uuid from 'uuid';
+import Sortable from 'sortablejs';
+import I from '../../common/i';
+import { resolveSrv } from 'dns';
+
+export default {
+	props: {
+		customize: Boolean,
+		mode: {
+			type: String,
+			default: 'timeline'
+		}
+	},
+	data() {
+		return {
+			home: [],
+			bakedHomeData: null
+		};
+	},
+	methods: {
+		bakeHomeData() {
+			return JSON.stringify(this.I.client_settings.home);
+		},
+		onTlLoaded() {
+			this.$emit('loaded');
+		},
+		onMeRefreshed() {
+			if (this.bakedHomeData != this.bakeHomeData()) {
+				// TODO: i18n
+				alert('別の場所でホームが編集されました。ページを再度読み込みすると編集が反映されます。');
+			}
+		},
+		onWidgetContextmenu(widgetId) {
+			this.$refs[widgetId].func();
+		},
+		addWidget() {
+			const widget = {
+				name: this.$refs.widgetSelector.options[this.$refs.widgetSelector.selectedIndex].value,
+				id: uuid(),
+				place: 'left',
+				data: {}
+			};
+
+			this.I.client_settings.home.unshift(widget);
+
+			this.saveHome();
+		},
+		saveHome() {
+			/*const data = [];
+
+			Array.from(this.$refs.left.children).forEach(el => {
+				const id = el.getAttribute('data-widget-id');
+				const widget = this.I.client_settings.home.find(w => w.id == id);
+				widget.place = 'left';
+				data.push(widget);
+			});
+
+			Array.from(this.$refs.right.children).forEach(el => {
+				const id = el.getAttribute('data-widget-id');
+				const widget = this.I.client_settings.home.find(w => w.id == id);
+				widget.place = 'right';
+				data.push(widget);
+			});
+
+			Array.from(this.$refs.maintop.children).forEach(el => {
+				const id = el.getAttribute('data-widget-id');
+				const widget = this.I.client_settings.home.find(w => w.id == id);
+				widget.place = 'main';
+				data.push(widget);
+			});
+
+			this.api('i/update_home', {
+				home: data
+			}).then(() => {
+				this.I.update();
+			});*/
+		}
+	},
+	computed: {
+		leftWidgets() {
+			return this.I.client_settings.home.filter(w => w.place == 'left');
+		},
+		centerWidgets() {
+			return this.I.client_settings.home.filter(w => w.place == 'center');
+		},
+		rightWidgets() {
+			return this.I.client_settings.home.filter(w => w.place == 'right');
+		}
+	},
+	created() {
+		this.bakedHomeData = this.bakeHomeData();
+	},
+	mounted() {
+		this.I.on('refreshed', this.onMeRefreshed);
+
+		this.I.client_settings.home.forEach(widget => {
+			try {
+				this.setWidget(widget);
+			} catch (e) {
+				// noop
+			}
+		});
+
+		if (!this.opts.customize) {
+			if (this.$refs.left.children.length == 0) {
+				this.$refs.left.parentNode.removeChild(this.$refs.left);
+			}
+			if (this.$refs.right.children.length == 0) {
+				this.$refs.right.parentNode.removeChild(this.$refs.right);
+			}
+		}
+
+		if (this.opts.customize) {
+			dialog('%fa:info-circle%カスタマイズのヒント',
+				'<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' +
+				'<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' +
+				'<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' +
+				'<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>',
+			[{
+				text: 'Got it!'
+			}]);
+
+			const sortableOption = {
+				group: 'kyoppie',
+				animation: 150,
+				onMove: evt => {
+					const id = evt.dragged.getAttribute('data-widget-id');
+					this.home.find(tag => tag.id == id).update({ place: evt.to.getAttribute('data-place') });
+				},
+				onSort: () => {
+					this.saveHome();
+				}
+			};
+
+			new Sortable(this.$refs.left, sortableOption);
+			new Sortable(this.$refs.right, sortableOption);
+			new Sortable(this.$refs.maintop, sortableOption);
+			new Sortable(this.$refs.trash, Object.assign({}, sortableOption, {
+				onAdd: evt => {
+					const el = evt.item;
+					const id = el.getAttribute('data-widget-id');
+					el.parentNode.removeChild(el);
+					this.I.client_settings.home = this.I.client_settings.home.filter(w => w.id != id);
+					this.saveHome();
+				}
+			}));
+		}
+	},
+	beforeDestroy() {
+		this.I.off('refreshed', this.onMeRefreshed);
+
+		this.home.forEach(widget => {
+			widget.unmount();
+		});
+	}
+};
+</script>
+
 <style lang="stylus" scoped>
 	:scope
 		display block
@@ -184,209 +370,3 @@
 					margin 0 auto
 
 </style>
-
-<script lang="typescript">
-	import uuid from 'uuid';
-	import Sortable from 'sortablejs';
-	import dialog from '../scripts/dialog';
-	import ScrollFollower from '../scripts/scroll-follower';
-
-	this.mixin('i');
-	this.mixin('api');
-
-	this.mode = this.opts.mode || 'timeline';
-
-	this.home = [];
-
-	this.bakeHomeData = () => JSON.stringify(this.I.client_settings.home);
-	this.bakedHomeData = this.bakeHomeData();
-
-	this.on('mount', () => {
-		this.$refs.tl.on('loaded', () => {
-			this.trigger('loaded');
-		});
-
-		this.I.on('refreshed', this.onMeRefreshed);
-
-		this.I.client_settings.home.forEach(widget => {
-			try {
-				this.setWidget(widget);
-			} catch (e) {
-				// noop
-			}
-		});
-
-		if (!this.opts.customize) {
-			if (this.$refs.left.children.length == 0) {
-				this.$refs.left.parentNode.removeChild(this.$refs.left);
-			}
-			if (this.$refs.right.children.length == 0) {
-				this.$refs.right.parentNode.removeChild(this.$refs.right);
-			}
-		}
-
-		if (this.opts.customize) {
-			dialog('%fa:info-circle%カスタマイズのヒント',
-				'<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' +
-				'<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' +
-				'<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' +
-				'<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>',
-			[{
-				text: 'Got it!'
-			}]);
-
-			const sortableOption = {
-				group: 'kyoppie',
-				animation: 150,
-				onMove: evt => {
-					const id = evt.dragged.getAttribute('data-widget-id');
-					this.home.find(tag => tag.id == id).update({ place: evt.to.getAttribute('data-place') });
-				},
-				onSort: () => {
-					this.saveHome();
-				}
-			};
-
-			new Sortable(this.$refs.left, sortableOption);
-			new Sortable(this.$refs.right, sortableOption);
-			new Sortable(this.$refs.maintop, sortableOption);
-			new Sortable(this.$refs.trash, Object.assign({}, sortableOption, {
-				onAdd: evt => {
-					const el = evt.item;
-					const id = el.getAttribute('data-widget-id');
-					el.parentNode.removeChild(el);
-					this.I.client_settings.home = this.I.client_settings.home.filter(w => w.id != id);
-					this.saveHome();
-				}
-			}));
-		}
-
-		if (!this.opts.customize) {
-			this.scrollFollowerLeft = this.$refs.left.parentNode ? new ScrollFollower(this.$refs.left, this.root.getBoundingClientRect().top) : null;
-			this.scrollFollowerRight = this.$refs.right.parentNode ? new ScrollFollower(this.$refs.right, this.root.getBoundingClientRect().top) : null;
-		}
-	});
-
-	this.on('unmount', () => {
-		this.I.off('refreshed', this.onMeRefreshed);
-
-		this.home.forEach(widget => {
-			widget.unmount();
-		});
-
-		if (!this.opts.customize) {
-			if (this.scrollFollowerLeft) this.scrollFollowerLeft.dispose();
-			if (this.scrollFollowerRight) this.scrollFollowerRight.dispose();
-		}
-	});
-
-	this.onMeRefreshed = () => {
-		if (this.bakedHomeData != this.bakeHomeData()) {
-			alert('別の場所でホームが編集されました。ページを再度読み込みすると編集が反映されます。');
-		}
-	};
-
-	this.setWidget = (widget, prepend = false) => {
-		const el = document.createElement(`mk-${widget.name}-home-widget`);
-
-		let actualEl;
-
-		if (this.opts.customize) {
-			const container = document.createElement('div');
-			container.classList.add('customize-container');
-			container.setAttribute('data-widget-id', widget.id);
-			container.appendChild(el);
-			actualEl = container;
-		} else {
-			actualEl = el;
-		}
-
-		switch (widget.place) {
-			case 'left':
-				if (prepend) {
-					this.$refs.left.insertBefore(actualEl, this.$refs.left.firstChild);
-				} else {
-					this.$refs.left.appendChild(actualEl);
-				}
-				break;
-			case 'right':
-				if (prepend) {
-					this.$refs.right.insertBefore(actualEl, this.$refs.right.firstChild);
-				} else {
-					this.$refs.right.appendChild(actualEl);
-				}
-				break;
-			case 'main':
-				if (this.opts.customize) {
-					this.$refs.maintop.appendChild(actualEl);
-				} else {
-					this.$refs.main.insertBefore(actualEl, this.$refs.tl.root);
-				}
-				break;
-		}
-
-		const tag = riot.mount(el, {
-			id: widget.id,
-			data: widget.data,
-			place: widget.place,
-			tl: this.$refs.tl
-		})[0];
-
-		this.home.push(tag);
-
-		if (this.opts.customize) {
-			actualEl.oncontextmenu = e => {
-				e.preventDefault();
-				e.stopImmediatePropagation();
-				if (tag.func) tag.func();
-				return false;
-			};
-		}
-	};
-
-	this.addWidget = () => {
-		const widget = {
-			name: this.$refs.widgetSelector.options[this.$refs.widgetSelector.selectedIndex].value,
-			id: uuid(),
-			place: 'left',
-			data: {}
-		};
-
-		this.I.client_settings.home.unshift(widget);
-
-		this.setWidget(widget, true);
-
-		this.saveHome();
-	};
-
-	this.saveHome = () => {
-		const data = [];
-
-		Array.from(this.$refs.left.children).forEach(el => {
-			const id = el.getAttribute('data-widget-id');
-			const widget = this.I.client_settings.home.find(w => w.id == id);
-			widget.place = 'left';
-			data.push(widget);
-		});
-
-		Array.from(this.$refs.right.children).forEach(el => {
-			const id = el.getAttribute('data-widget-id');
-			const widget = this.I.client_settings.home.find(w => w.id == id);
-			widget.place = 'right';
-			data.push(widget);
-		});
-
-		Array.from(this.$refs.maintop.children).forEach(el => {
-			const id = el.getAttribute('data-widget-id');
-			const widget = this.I.client_settings.home.find(w => w.id == id);
-			widget.place = 'main';
-			data.push(widget);
-		});
-
-		this.api('i/update_home', {
-			home: data
-		}).then(() => {
-			this.I.update();
-		});
-	};
-</script>
diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag
index 358deb82f..ddbb485d9 100644
--- a/src/web/app/desktop/tags/post-form.tag
+++ b/src/web/app/desktop/tags/post-form.tag
@@ -324,7 +324,7 @@
 			});
 
 			this.$refs.uploader.on('change-uploads', uploads => {
-				this.trigger('change-uploading-files', uploads);
+				this.$emit('change-uploading-files', uploads);
 			});
 
 			this.autocomplete = new Autocomplete(this.$refs.text);
@@ -340,7 +340,7 @@
 					this.update();
 					this.$refs.poll.set(draft.data.poll);
 				}
-				this.trigger('change-files', this.files);
+				this.$emit('change-files', this.files);
 				this.update();
 			}
 
@@ -361,7 +361,7 @@
 			this.$refs.text.value = '';
 			this.files = [];
 			this.poll = false;
-			this.trigger('change-files');
+			this.$emit('change-files');
 			this.update();
 		};
 
@@ -444,14 +444,14 @@
 
 		this.addFile = file => {
 			this.files.push(file);
-			this.trigger('change-files', this.files);
+			this.$emit('change-files', this.files);
 			this.update();
 		};
 
 		this.removeFile = e => {
 			const file = e.item;
 			this.files = this.files.filter(x => x.id != file.id);
-			this.trigger('change-files', this.files);
+			this.$emit('change-files', this.files);
 			this.update();
 		};
 
@@ -487,7 +487,7 @@
 			}).then(data => {
 				this.clear();
 				this.removeDraft();
-				this.trigger('post');
+				this.$emit('post');
 				notify(this.repost
 					? '%i18n:desktop.tags.mk-post-form.reposted%'
 					: this.inReplyToPost
diff --git a/src/web/app/desktop/tags/repost-form.tag b/src/web/app/desktop/tags/repost-form.tag
index a3d350fa2..afe555b6d 100644
--- a/src/web/app/desktop/tags/repost-form.tag
+++ b/src/web/app/desktop/tags/repost-form.tag
@@ -93,7 +93,7 @@
 		this.quote = false;
 
 		this.cancel = () => {
-			this.trigger('cancel');
+			this.$emit('cancel');
 		};
 
 		this.ok = () => {
@@ -101,7 +101,7 @@
 			this.api('posts/create', {
 				repost_id: this.opts.post.id
 			}).then(data => {
-				this.trigger('posted');
+				this.$emit('posted');
 				notify('%i18n:desktop.tags.mk-repost-form.success%');
 			}).catch(err => {
 				notify('%i18n:desktop.tags.mk-repost-form.failure%');
@@ -118,7 +118,7 @@
 			});
 
 			this.$refs.form.on('post', () => {
-				this.trigger('posted');
+				this.$emit('posted');
 			});
 
 			this.$refs.form.focus();
diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag
index 91bea2e90..52c68b754 100644
--- a/src/web/app/desktop/tags/search-posts.tag
+++ b/src/web/app/desktop/tags/search-posts.tag
@@ -54,7 +54,7 @@
 					isEmpty: posts.length == 0
 				});
 				this.$refs.timeline.setPosts(posts);
-				this.trigger('loaded');
+				this.$emit('loaded');
 			});
 		});
 
diff --git a/src/web/app/desktop/tags/search.tag b/src/web/app/desktop/tags/search.tag
index ec6bbfc34..28127b721 100644
--- a/src/web/app/desktop/tags/search.tag
+++ b/src/web/app/desktop/tags/search.tag
@@ -27,7 +27,7 @@
 
 		this.on('mount', () => {
 			this.$refs.posts.on('loaded', () => {
-				this.trigger('loaded');
+				this.$emit('loaded');
 			});
 		});
 	</script>
diff --git a/src/web/app/desktop/tags/select-file-from-drive-window.tag b/src/web/app/desktop/tags/select-file-from-drive-window.tag
index 10dc7db9f..d6234d5fd 100644
--- a/src/web/app/desktop/tags/select-file-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-file-from-drive-window.tag
@@ -166,7 +166,7 @@
 		};
 
 		this.ok = () => {
-			this.trigger('selected', this.multiple ? this.files : this.files[0]);
+			this.$emit('selected', this.multiple ? this.files : this.files[0]);
 			this.$refs.window.close();
 		};
 	</script>
diff --git a/src/web/app/desktop/tags/select-folder-from-drive-window.tag b/src/web/app/desktop/tags/select-folder-from-drive-window.tag
index 1cd7527c8..2f98f30a6 100644
--- a/src/web/app/desktop/tags/select-folder-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-folder-from-drive-window.tag
@@ -105,7 +105,7 @@
 		};
 
 		this.ok = () => {
-			this.trigger('selected', this.$refs.window.refs.browser.folder);
+			this.$emit('selected', this.$refs.window.refs.browser.folder);
 			this.$refs.window.close();
 		};
 	</script>
diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/tags/user-timeline.tag
index 2e3bbbfd6..f018ba64e 100644
--- a/src/web/app/desktop/tags/user-timeline.tag
+++ b/src/web/app/desktop/tags/user-timeline.tag
@@ -76,7 +76,7 @@
 					user: user
 				});
 
-				this.fetch(() => this.trigger('loaded'));
+				this.fetch(() => this.$emit('loaded'));
 			});
 		});
 
diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/tags/user.tag
index 161a15190..8221926f4 100644
--- a/src/web/app/desktop/tags/user.tag
+++ b/src/web/app/desktop/tags/user.tag
@@ -32,7 +32,7 @@
 					fetching: false,
 					user: user
 				});
-				this.trigger('loaded');
+				this.$emit('loaded');
 			});
 		});
 	</script>
@@ -716,7 +716,7 @@
 
 		this.on('mount', () => {
 			this.$refs.tl.on('loaded', () => {
-				this.trigger('loaded');
+				this.$emit('loaded');
 			});
 
 			this.scrollFollowerLeft = new ScrollFollower(this.$refs.left, this.parent.root.getBoundingClientRect().top);
@@ -778,7 +778,7 @@
 	</style>
 	<script lang="typescript">
 		this.on('mount', () => {
-			this.trigger('loaded');
+			this.$emit('loaded');
 		});
 	</script>
 </mk-user-graphs>
diff --git a/src/web/app/desktop/tags/users-list.tag b/src/web/app/desktop/tags/users-list.tag
index 90173bfd2..bf002ae55 100644
--- a/src/web/app/desktop/tags/users-list.tag
+++ b/src/web/app/desktop/tags/users-list.tag
@@ -98,7 +98,7 @@
 		this.moreFetching = false;
 
 		this.on('mount', () => {
-			this.fetch(() => this.trigger('loaded'));
+			this.fetch(() => this.$emit('loaded'));
 		});
 
 		this.fetch = cb => {
diff --git a/src/web/app/desktop/tags/widgets/activity.tag b/src/web/app/desktop/tags/widgets/activity.tag
index ffddfa7dc..8c20ef5a6 100644
--- a/src/web/app/desktop/tags/widgets/activity.tag
+++ b/src/web/app/desktop/tags/widgets/activity.tag
@@ -82,7 +82,7 @@
 			this.view++;
 			if (this.view == 2) this.view = 0;
 			this.update();
-			this.trigger('view-changed', this.view);
+			this.$emit('view-changed', this.view);
 		};
 	</script>
 </mk-activity-widget>
diff --git a/src/web/app/desktop/tags/window.tag b/src/web/app/desktop/tags/window.tag
index dc7a37fff..051b43f07 100644
--- a/src/web/app/desktop/tags/window.tag
+++ b/src/web/app/desktop/tags/window.tag
@@ -231,7 +231,7 @@
 		};
 
 		this.open = () => {
-			this.trigger('opening');
+			this.$emit('opening');
 
 			this.top();
 
@@ -257,7 +257,7 @@
 			//this.$refs.main.focus();
 
 			setTimeout(() => {
-				this.trigger('opened');
+				this.$emit('opened');
 			}, 300);
 		};
 
@@ -278,7 +278,7 @@
 		};
 
 		this.close = () => {
-			this.trigger('closing');
+			this.$emit('closing');
 
 			if (this.isModal) {
 				this.$refs.bg.style.pointerEvents = 'none';
@@ -301,7 +301,7 @@
 			});
 
 			setTimeout(() => {
-				this.trigger('closed');
+				this.$emit('closed');
 			}, 300);
 		};
 
diff --git a/src/web/app/mobile/tags/drive-folder-selector.tag b/src/web/app/mobile/tags/drive-folder-selector.tag
index a63d90af5..7dca527d6 100644
--- a/src/web/app/mobile/tags/drive-folder-selector.tag
+++ b/src/web/app/mobile/tags/drive-folder-selector.tag
@@ -57,12 +57,12 @@
 	</style>
 	<script lang="typescript">
 		this.cancel = () => {
-			this.trigger('canceled');
+			this.$emit('canceled');
 			this.$destroy();
 		};
 
 		this.ok = () => {
-			this.trigger('selected', this.$refs.browser.folder);
+			this.$emit('selected', this.$refs.browser.folder);
 			this.$destroy();
 		};
 	</script>
diff --git a/src/web/app/mobile/tags/drive-selector.tag b/src/web/app/mobile/tags/drive-selector.tag
index d3e4f54c2..4589592a7 100644
--- a/src/web/app/mobile/tags/drive-selector.tag
+++ b/src/web/app/mobile/tags/drive-selector.tag
@@ -70,18 +70,18 @@
 			});
 
 			this.$refs.browser.on('selected', file => {
-				this.trigger('selected', file);
+				this.$emit('selected', file);
 				this.$destroy();
 			});
 		});
 
 		this.cancel = () => {
-			this.trigger('canceled');
+			this.$emit('canceled');
 			this.$destroy();
 		};
 
 		this.ok = () => {
-			this.trigger('selected', this.files);
+			this.$emit('selected', this.files);
 			this.$destroy();
 		};
 	</script>
diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index 50578299a..a7a8a35c3 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -274,7 +274,7 @@
 				if (folder.parent) dive(folder.parent);
 
 				this.update();
-				this.trigger('open-folder', this.folder, silent);
+				this.$emit('open-folder', this.folder, silent);
 				this.fetch();
 			});
 		};
@@ -343,7 +343,7 @@
 					folder: null,
 					hierarchyFolders: []
 				});
-				this.trigger('move-root');
+				this.$emit('move-root');
 				this.fetch();
 			}
 
@@ -359,7 +359,7 @@
 				fetching: true
 			});
 
-			this.trigger('begin-fetch');
+			this.$emit('begin-fetch');
 
 			let fetchedFolders = null;
 			let fetchedFiles = null;
@@ -402,11 +402,11 @@
 						fetching: false
 					});
 					// 一連の読み込みが完了したイベントを発行
-					this.trigger('fetched');
+					this.$emit('fetched');
 				} else {
 					flag = true;
 					// 一連の読み込みが半分完了したイベントを発行
-					this.trigger('fetch-mid');
+					this.$emit('fetch-mid');
 				}
 			};
 
@@ -455,9 +455,9 @@
 						this.selectedFiles.push(file);
 					}
 					this.update();
-					this.trigger('change-selection', this.selectedFiles);
+					this.$emit('change-selection', this.selectedFiles);
 				} else {
-					this.trigger('selected', file);
+					this.$emit('selected', file);
 				}
 			} else {
 				this.cf(file);
@@ -482,7 +482,7 @@
 				if (file.folder) dive(file.folder);
 
 				this.update();
-				this.trigger('open-file', this.file, silent);
+				this.$emit('open-file', this.file, silent);
 			});
 		};
 
diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag
index 70074ef9f..88e26bc78 100644
--- a/src/web/app/mobile/tags/home-timeline.tag
+++ b/src/web/app/mobile/tags/home-timeline.tag
@@ -22,7 +22,7 @@
 		this.init = new Promise((res, rej) => {
 			this.api('posts/timeline').then(posts => {
 				res(posts);
-				this.trigger('loaded');
+				this.$emit('loaded');
 			});
 		});
 
diff --git a/src/web/app/mobile/tags/home.tag b/src/web/app/mobile/tags/home.tag
index a304708b3..038322b63 100644
--- a/src/web/app/mobile/tags/home.tag
+++ b/src/web/app/mobile/tags/home.tag
@@ -16,7 +16,7 @@
 	<script lang="typescript">
 		this.on('mount', () => {
 			this.$refs.tl.on('loaded', () => {
-				this.trigger('loaded');
+				this.$emit('loaded');
 			});
 		});
 	</script>
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index c945f6a3e..8a1482aca 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -106,7 +106,7 @@
 					notifications: notifications
 				});
 
-				this.trigger('fetched');
+				this.$emit('fetched');
 			});
 
 			this.connection.on('notification', this.onNotification);
diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
index 1c0282e77..a37e2bf38 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -161,7 +161,7 @@
 			});
 
 			this.$refs.uploader.on('change-uploads', uploads => {
-				this.trigger('change-uploading-files', uploads);
+				this.$emit('change-uploading-files', uploads);
 			});
 
 			this.$refs.text.focus();
@@ -207,19 +207,19 @@
 		this.addFile = file => {
 			file._remove = () => {
 				this.files = this.files.filter(x => x.id != file.id);
-				this.trigger('change-files', this.files);
+				this.$emit('change-files', this.files);
 				this.update();
 			};
 
 			this.files.push(file);
-			this.trigger('change-files', this.files);
+			this.$emit('change-files', this.files);
 			this.update();
 		};
 
 		this.removeFile = e => {
 			const file = e.item;
 			this.files = this.files.filter(x => x.id != file.id);
-			this.trigger('change-files', this.files);
+			this.$emit('change-files', this.files);
 			this.update();
 		};
 
@@ -254,7 +254,7 @@
 				reply_id: opts.reply ? opts.reply.id : undefined,
 				poll: this.poll ? this.$refs.poll.get() : undefined
 			}).then(data => {
-				this.trigger('post');
+				this.$emit('post');
 				this.$destroy();
 			}).catch(err => {
 				this.update({
@@ -264,7 +264,7 @@
 		};
 
 		this.cancel = () => {
-			this.trigger('cancel');
+			this.$emit('cancel');
 			this.$destroy();
 		};
 
diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag
index 00936a838..c650fbce5 100644
--- a/src/web/app/mobile/tags/search-posts.tag
+++ b/src/web/app/mobile/tags/search-posts.tag
@@ -27,7 +27,7 @@
 		this.init = new Promise((res, rej) => {
 			this.api('posts/search', parse(this.query)).then(posts => {
 				res(posts);
-				this.trigger('loaded');
+				this.$emit('loaded');
 			});
 		});
 
diff --git a/src/web/app/mobile/tags/search.tag b/src/web/app/mobile/tags/search.tag
index 36f375e96..61f3093e0 100644
--- a/src/web/app/mobile/tags/search.tag
+++ b/src/web/app/mobile/tags/search.tag
@@ -9,7 +9,7 @@
 
 		this.on('mount', () => {
 			this.$refs.posts.on('loaded', () => {
-				this.trigger('loaded');
+				this.$emit('loaded');
 			});
 		});
 	</script>
diff --git a/src/web/app/mobile/tags/user-followers.tag b/src/web/app/mobile/tags/user-followers.tag
index 02368045e..b9101e212 100644
--- a/src/web/app/mobile/tags/user-followers.tag
+++ b/src/web/app/mobile/tags/user-followers.tag
@@ -21,7 +21,7 @@
 
 		this.on('mount', () => {
 			this.$refs.list.on('loaded', () => {
-				this.trigger('loaded');
+				this.$emit('loaded');
 			});
 		});
 	</script>
diff --git a/src/web/app/mobile/tags/user-following.tag b/src/web/app/mobile/tags/user-following.tag
index c0eb58b4b..5cfe60fec 100644
--- a/src/web/app/mobile/tags/user-following.tag
+++ b/src/web/app/mobile/tags/user-following.tag
@@ -21,7 +21,7 @@
 
 		this.on('mount', () => {
 			this.$refs.list.on('loaded', () => {
-				this.trigger('loaded');
+				this.$emit('loaded');
 			});
 		});
 	</script>
diff --git a/src/web/app/mobile/tags/user-timeline.tag b/src/web/app/mobile/tags/user-timeline.tag
index 270a3744c..b9f5dfbd5 100644
--- a/src/web/app/mobile/tags/user-timeline.tag
+++ b/src/web/app/mobile/tags/user-timeline.tag
@@ -18,7 +18,7 @@
 				with_media: this.withMedia
 			}).then(posts => {
 				res(posts);
-				this.trigger('loaded');
+				this.$emit('loaded');
 			});
 		});
 
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index 0091bafc2..87e63471e 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -201,7 +201,7 @@
 			}).then(user => {
 				this.fetching = false;
 				this.user = user;
-				this.trigger('loaded', user);
+				this.$emit('loaded', user);
 				this.update();
 			});
 		});
diff --git a/src/web/app/mobile/tags/users-list.tag b/src/web/app/mobile/tags/users-list.tag
index fb7040a7a..2bc0c6e93 100644
--- a/src/web/app/mobile/tags/users-list.tag
+++ b/src/web/app/mobile/tags/users-list.tag
@@ -87,7 +87,7 @@
 		this.moreFetching = false;
 
 		this.on('mount', () => {
-			this.fetch(() => this.trigger('loaded'));
+			this.fetch(() => this.$emit('loaded'));
 		});
 
 		this.fetch = cb => {

From a45d2b2293fd6aa35b8509d22e9b67df052341c8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Feb 2018 13:20:45 +0900
Subject: [PATCH 020/286] wip

---
 src/web/app/common/{tags => -tags}/activity-table.tag             | 0
 src/web/app/common/{tags => -tags}/authorized-apps.tag            | 0
 src/web/app/common/{tags => -tags}/ellipsis.tag                   | 0
 src/web/app/common/{tags => -tags}/error.tag                      | 0
 src/web/app/common/{tags => -tags}/file-type-icon.tag             | 0
 src/web/app/common/{tags => -tags}/forkit.tag                     | 0
 src/web/app/common/{tags => -tags}/index.ts                       | 0
 src/web/app/common/{tags => -tags}/introduction.tag               | 0
 src/web/app/common/{tags => -tags}/messaging/form.tag             | 0
 src/web/app/common/{tags => -tags}/messaging/index.tag            | 0
 src/web/app/common/{tags => -tags}/messaging/message.tag          | 0
 src/web/app/common/{tags => -tags}/messaging/room.tag             | 0
 src/web/app/common/{tags => -tags}/nav-links.tag                  | 0
 src/web/app/common/{tags => -tags}/number.tag                     | 0
 src/web/app/common/{tags => -tags}/poll-editor.tag                | 0
 src/web/app/common/{tags => -tags}/post-menu.tag                  | 0
 src/web/app/common/{tags => -tags}/raw.tag                        | 0
 src/web/app/common/{tags => -tags}/signin-history.tag             | 0
 src/web/app/common/{tags => -tags}/signin.tag                     | 0
 src/web/app/common/{tags => -tags}/signup.tag                     | 0
 src/web/app/common/{tags => -tags}/special-message.tag            | 0
 src/web/app/common/{tags => -tags}/twitter-setting.tag            | 0
 src/web/app/common/{tags => -tags}/uploader.tag                   | 0
 src/web/app/desktop/{tags => -tags}/analog-clock.tag              | 0
 src/web/app/desktop/{tags => -tags}/autocomplete-suggestion.tag   | 0
 src/web/app/desktop/{tags => -tags}/big-follow-button.tag         | 0
 src/web/app/desktop/{tags => -tags}/contextmenu.tag               | 0
 src/web/app/desktop/{tags => -tags}/crop-window.tag               | 0
 src/web/app/desktop/{tags => -tags}/detailed-post-window.tag      | 0
 src/web/app/desktop/{tags => -tags}/dialog.tag                    | 0
 src/web/app/desktop/{tags => -tags}/donation.tag                  | 0
 src/web/app/desktop/{tags => -tags}/drive/base-contextmenu.tag    | 0
 src/web/app/desktop/{tags => -tags}/drive/browser-window.tag      | 0
 src/web/app/desktop/{tags => -tags}/drive/browser.tag             | 0
 src/web/app/desktop/{tags => -tags}/drive/file-contextmenu.tag    | 0
 src/web/app/desktop/{tags => -tags}/drive/file.tag                | 0
 src/web/app/desktop/{tags => -tags}/drive/folder-contextmenu.tag  | 0
 src/web/app/desktop/{tags => -tags}/drive/folder.tag              | 0
 src/web/app/desktop/{tags => -tags}/drive/nav-folder.tag          | 0
 src/web/app/desktop/{tags => -tags}/ellipsis-icon.tag             | 0
 src/web/app/desktop/{tags => -tags}/follow-button.tag             | 0
 src/web/app/desktop/{tags => -tags}/following-setuper.tag         | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/access-log.tag   | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/activity.tag     | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/broadcast.tag    | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/calendar.tag     | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/channel.tag      | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/donation.tag     | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/mentions.tag     | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/messaging.tag    | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/nav.tag          | 0
 .../app/desktop/{tags => -tags}/home-widgets/notifications.tag    | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/photo-stream.tag | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/post-form.tag    | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/profile.tag      | 0
 .../desktop/{tags => -tags}/home-widgets/recommended-polls.tag    | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/rss-reader.tag   | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/server.tag       | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/slideshow.tag    | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/timeline.tag     | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/timemachine.tag  | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/tips.tag         | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/trends.tag       | 0
 .../desktop/{tags => -tags}/home-widgets/user-recommendation.tag  | 0
 src/web/app/desktop/{tags => -tags}/home-widgets/version.tag      | 0
 src/web/app/desktop/{tags => -tags}/images.tag                    | 0
 src/web/app/desktop/{tags => -tags}/index.ts                      | 0
 src/web/app/desktop/{tags => -tags}/input-dialog.tag              | 0
 src/web/app/desktop/{tags => -tags}/list-user.tag                 | 0
 src/web/app/desktop/{tags => -tags}/messaging/room-window.tag     | 0
 src/web/app/desktop/{tags => -tags}/messaging/window.tag          | 0
 src/web/app/desktop/{tags => -tags}/notifications.tag             | 0
 src/web/app/desktop/{tags => -tags}/pages/drive.tag               | 0
 src/web/app/desktop/{tags => -tags}/pages/entrance.tag            | 0
 src/web/app/desktop/{tags => -tags}/pages/home-customize.tag      | 0
 src/web/app/desktop/{tags => -tags}/pages/home.tag                | 0
 src/web/app/desktop/{tags => -tags}/pages/messaging-room.tag      | 0
 src/web/app/desktop/{tags => -tags}/pages/not-found.tag           | 0
 src/web/app/desktop/{tags => -tags}/pages/post.tag                | 0
 src/web/app/desktop/{tags => -tags}/pages/search.tag              | 0
 src/web/app/desktop/{tags => -tags}/pages/selectdrive.tag         | 0
 src/web/app/desktop/{tags => -tags}/pages/user.tag                | 0
 src/web/app/desktop/{tags => -tags}/post-detail-sub.tag           | 0
 src/web/app/desktop/{tags => -tags}/post-detail.tag               | 0
 src/web/app/desktop/{tags => -tags}/post-form-window.tag          | 0
 src/web/app/desktop/{tags => -tags}/post-form.tag                 | 0
 src/web/app/desktop/{tags => -tags}/post-preview.tag              | 0
 src/web/app/desktop/{tags => -tags}/progress-dialog.tag           | 0
 src/web/app/desktop/{tags => -tags}/repost-form-window.tag        | 0
 src/web/app/desktop/{tags => -tags}/repost-form.tag               | 0
 src/web/app/desktop/{tags => -tags}/search-posts.tag              | 0
 src/web/app/desktop/{tags => -tags}/search.tag                    | 0
 .../app/desktop/{tags => -tags}/select-file-from-drive-window.tag | 0
 .../desktop/{tags => -tags}/select-folder-from-drive-window.tag   | 0
 src/web/app/desktop/{tags => -tags}/set-avatar-suggestion.tag     | 0
 src/web/app/desktop/{tags => -tags}/set-banner-suggestion.tag     | 0
 src/web/app/desktop/{tags => -tags}/settings-window.tag           | 0
 src/web/app/desktop/{tags => -tags}/settings.tag                  | 0
 src/web/app/desktop/{tags => -tags}/sub-post-content.tag          | 0
 src/web/app/desktop/{tags => -tags}/timeline.tag                  | 0
 src/web/app/desktop/{tags => -tags}/ui.tag                        | 0
 src/web/app/desktop/{tags => -tags}/user-followers-window.tag     | 0
 src/web/app/desktop/{tags => -tags}/user-followers.tag            | 0
 src/web/app/desktop/{tags => -tags}/user-following-window.tag     | 0
 src/web/app/desktop/{tags => -tags}/user-following.tag            | 0
 src/web/app/desktop/{tags => -tags}/user-preview.tag              | 0
 src/web/app/desktop/{tags => -tags}/user-timeline.tag             | 0
 src/web/app/desktop/{tags => -tags}/user.tag                      | 0
 src/web/app/desktop/{tags => -tags}/users-list.tag                | 0
 src/web/app/desktop/{tags => -tags}/widgets/activity.tag          | 0
 src/web/app/desktop/{tags => -tags}/widgets/calendar.tag          | 0
 src/web/app/desktop/{tags => -tags}/window.tag                    | 0
 112 files changed, 0 insertions(+), 0 deletions(-)
 rename src/web/app/common/{tags => -tags}/activity-table.tag (100%)
 rename src/web/app/common/{tags => -tags}/authorized-apps.tag (100%)
 rename src/web/app/common/{tags => -tags}/ellipsis.tag (100%)
 rename src/web/app/common/{tags => -tags}/error.tag (100%)
 rename src/web/app/common/{tags => -tags}/file-type-icon.tag (100%)
 rename src/web/app/common/{tags => -tags}/forkit.tag (100%)
 rename src/web/app/common/{tags => -tags}/index.ts (100%)
 rename src/web/app/common/{tags => -tags}/introduction.tag (100%)
 rename src/web/app/common/{tags => -tags}/messaging/form.tag (100%)
 rename src/web/app/common/{tags => -tags}/messaging/index.tag (100%)
 rename src/web/app/common/{tags => -tags}/messaging/message.tag (100%)
 rename src/web/app/common/{tags => -tags}/messaging/room.tag (100%)
 rename src/web/app/common/{tags => -tags}/nav-links.tag (100%)
 rename src/web/app/common/{tags => -tags}/number.tag (100%)
 rename src/web/app/common/{tags => -tags}/poll-editor.tag (100%)
 rename src/web/app/common/{tags => -tags}/post-menu.tag (100%)
 rename src/web/app/common/{tags => -tags}/raw.tag (100%)
 rename src/web/app/common/{tags => -tags}/signin-history.tag (100%)
 rename src/web/app/common/{tags => -tags}/signin.tag (100%)
 rename src/web/app/common/{tags => -tags}/signup.tag (100%)
 rename src/web/app/common/{tags => -tags}/special-message.tag (100%)
 rename src/web/app/common/{tags => -tags}/twitter-setting.tag (100%)
 rename src/web/app/common/{tags => -tags}/uploader.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/analog-clock.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/autocomplete-suggestion.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/big-follow-button.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/contextmenu.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/crop-window.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/detailed-post-window.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/dialog.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/donation.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/drive/base-contextmenu.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/drive/browser-window.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/drive/browser.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/drive/file-contextmenu.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/drive/file.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/drive/folder-contextmenu.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/drive/folder.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/drive/nav-folder.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/ellipsis-icon.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/follow-button.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/following-setuper.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/access-log.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/activity.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/broadcast.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/calendar.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/channel.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/donation.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/mentions.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/messaging.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/nav.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/notifications.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/photo-stream.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/post-form.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/profile.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/recommended-polls.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/rss-reader.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/server.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/slideshow.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/timeline.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/timemachine.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/tips.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/trends.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/user-recommendation.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/home-widgets/version.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/images.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/index.ts (100%)
 rename src/web/app/desktop/{tags => -tags}/input-dialog.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/list-user.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/messaging/room-window.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/messaging/window.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/notifications.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/pages/drive.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/pages/entrance.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/pages/home-customize.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/pages/home.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/pages/messaging-room.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/pages/not-found.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/pages/post.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/pages/search.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/pages/selectdrive.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/pages/user.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/post-detail-sub.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/post-detail.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/post-form-window.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/post-form.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/post-preview.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/progress-dialog.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/repost-form-window.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/repost-form.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/search-posts.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/search.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/select-file-from-drive-window.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/select-folder-from-drive-window.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/set-avatar-suggestion.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/set-banner-suggestion.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/settings-window.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/settings.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/sub-post-content.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/timeline.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/ui.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/user-followers-window.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/user-followers.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/user-following-window.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/user-following.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/user-preview.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/user-timeline.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/user.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/users-list.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/widgets/activity.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/widgets/calendar.tag (100%)
 rename src/web/app/desktop/{tags => -tags}/window.tag (100%)

diff --git a/src/web/app/common/tags/activity-table.tag b/src/web/app/common/-tags/activity-table.tag
similarity index 100%
rename from src/web/app/common/tags/activity-table.tag
rename to src/web/app/common/-tags/activity-table.tag
diff --git a/src/web/app/common/tags/authorized-apps.tag b/src/web/app/common/-tags/authorized-apps.tag
similarity index 100%
rename from src/web/app/common/tags/authorized-apps.tag
rename to src/web/app/common/-tags/authorized-apps.tag
diff --git a/src/web/app/common/tags/ellipsis.tag b/src/web/app/common/-tags/ellipsis.tag
similarity index 100%
rename from src/web/app/common/tags/ellipsis.tag
rename to src/web/app/common/-tags/ellipsis.tag
diff --git a/src/web/app/common/tags/error.tag b/src/web/app/common/-tags/error.tag
similarity index 100%
rename from src/web/app/common/tags/error.tag
rename to src/web/app/common/-tags/error.tag
diff --git a/src/web/app/common/tags/file-type-icon.tag b/src/web/app/common/-tags/file-type-icon.tag
similarity index 100%
rename from src/web/app/common/tags/file-type-icon.tag
rename to src/web/app/common/-tags/file-type-icon.tag
diff --git a/src/web/app/common/tags/forkit.tag b/src/web/app/common/-tags/forkit.tag
similarity index 100%
rename from src/web/app/common/tags/forkit.tag
rename to src/web/app/common/-tags/forkit.tag
diff --git a/src/web/app/common/tags/index.ts b/src/web/app/common/-tags/index.ts
similarity index 100%
rename from src/web/app/common/tags/index.ts
rename to src/web/app/common/-tags/index.ts
diff --git a/src/web/app/common/tags/introduction.tag b/src/web/app/common/-tags/introduction.tag
similarity index 100%
rename from src/web/app/common/tags/introduction.tag
rename to src/web/app/common/-tags/introduction.tag
diff --git a/src/web/app/common/tags/messaging/form.tag b/src/web/app/common/-tags/messaging/form.tag
similarity index 100%
rename from src/web/app/common/tags/messaging/form.tag
rename to src/web/app/common/-tags/messaging/form.tag
diff --git a/src/web/app/common/tags/messaging/index.tag b/src/web/app/common/-tags/messaging/index.tag
similarity index 100%
rename from src/web/app/common/tags/messaging/index.tag
rename to src/web/app/common/-tags/messaging/index.tag
diff --git a/src/web/app/common/tags/messaging/message.tag b/src/web/app/common/-tags/messaging/message.tag
similarity index 100%
rename from src/web/app/common/tags/messaging/message.tag
rename to src/web/app/common/-tags/messaging/message.tag
diff --git a/src/web/app/common/tags/messaging/room.tag b/src/web/app/common/-tags/messaging/room.tag
similarity index 100%
rename from src/web/app/common/tags/messaging/room.tag
rename to src/web/app/common/-tags/messaging/room.tag
diff --git a/src/web/app/common/tags/nav-links.tag b/src/web/app/common/-tags/nav-links.tag
similarity index 100%
rename from src/web/app/common/tags/nav-links.tag
rename to src/web/app/common/-tags/nav-links.tag
diff --git a/src/web/app/common/tags/number.tag b/src/web/app/common/-tags/number.tag
similarity index 100%
rename from src/web/app/common/tags/number.tag
rename to src/web/app/common/-tags/number.tag
diff --git a/src/web/app/common/tags/poll-editor.tag b/src/web/app/common/-tags/poll-editor.tag
similarity index 100%
rename from src/web/app/common/tags/poll-editor.tag
rename to src/web/app/common/-tags/poll-editor.tag
diff --git a/src/web/app/common/tags/post-menu.tag b/src/web/app/common/-tags/post-menu.tag
similarity index 100%
rename from src/web/app/common/tags/post-menu.tag
rename to src/web/app/common/-tags/post-menu.tag
diff --git a/src/web/app/common/tags/raw.tag b/src/web/app/common/-tags/raw.tag
similarity index 100%
rename from src/web/app/common/tags/raw.tag
rename to src/web/app/common/-tags/raw.tag
diff --git a/src/web/app/common/tags/signin-history.tag b/src/web/app/common/-tags/signin-history.tag
similarity index 100%
rename from src/web/app/common/tags/signin-history.tag
rename to src/web/app/common/-tags/signin-history.tag
diff --git a/src/web/app/common/tags/signin.tag b/src/web/app/common/-tags/signin.tag
similarity index 100%
rename from src/web/app/common/tags/signin.tag
rename to src/web/app/common/-tags/signin.tag
diff --git a/src/web/app/common/tags/signup.tag b/src/web/app/common/-tags/signup.tag
similarity index 100%
rename from src/web/app/common/tags/signup.tag
rename to src/web/app/common/-tags/signup.tag
diff --git a/src/web/app/common/tags/special-message.tag b/src/web/app/common/-tags/special-message.tag
similarity index 100%
rename from src/web/app/common/tags/special-message.tag
rename to src/web/app/common/-tags/special-message.tag
diff --git a/src/web/app/common/tags/twitter-setting.tag b/src/web/app/common/-tags/twitter-setting.tag
similarity index 100%
rename from src/web/app/common/tags/twitter-setting.tag
rename to src/web/app/common/-tags/twitter-setting.tag
diff --git a/src/web/app/common/tags/uploader.tag b/src/web/app/common/-tags/uploader.tag
similarity index 100%
rename from src/web/app/common/tags/uploader.tag
rename to src/web/app/common/-tags/uploader.tag
diff --git a/src/web/app/desktop/tags/analog-clock.tag b/src/web/app/desktop/-tags/analog-clock.tag
similarity index 100%
rename from src/web/app/desktop/tags/analog-clock.tag
rename to src/web/app/desktop/-tags/analog-clock.tag
diff --git a/src/web/app/desktop/tags/autocomplete-suggestion.tag b/src/web/app/desktop/-tags/autocomplete-suggestion.tag
similarity index 100%
rename from src/web/app/desktop/tags/autocomplete-suggestion.tag
rename to src/web/app/desktop/-tags/autocomplete-suggestion.tag
diff --git a/src/web/app/desktop/tags/big-follow-button.tag b/src/web/app/desktop/-tags/big-follow-button.tag
similarity index 100%
rename from src/web/app/desktop/tags/big-follow-button.tag
rename to src/web/app/desktop/-tags/big-follow-button.tag
diff --git a/src/web/app/desktop/tags/contextmenu.tag b/src/web/app/desktop/-tags/contextmenu.tag
similarity index 100%
rename from src/web/app/desktop/tags/contextmenu.tag
rename to src/web/app/desktop/-tags/contextmenu.tag
diff --git a/src/web/app/desktop/tags/crop-window.tag b/src/web/app/desktop/-tags/crop-window.tag
similarity index 100%
rename from src/web/app/desktop/tags/crop-window.tag
rename to src/web/app/desktop/-tags/crop-window.tag
diff --git a/src/web/app/desktop/tags/detailed-post-window.tag b/src/web/app/desktop/-tags/detailed-post-window.tag
similarity index 100%
rename from src/web/app/desktop/tags/detailed-post-window.tag
rename to src/web/app/desktop/-tags/detailed-post-window.tag
diff --git a/src/web/app/desktop/tags/dialog.tag b/src/web/app/desktop/-tags/dialog.tag
similarity index 100%
rename from src/web/app/desktop/tags/dialog.tag
rename to src/web/app/desktop/-tags/dialog.tag
diff --git a/src/web/app/desktop/tags/donation.tag b/src/web/app/desktop/-tags/donation.tag
similarity index 100%
rename from src/web/app/desktop/tags/donation.tag
rename to src/web/app/desktop/-tags/donation.tag
diff --git a/src/web/app/desktop/tags/drive/base-contextmenu.tag b/src/web/app/desktop/-tags/drive/base-contextmenu.tag
similarity index 100%
rename from src/web/app/desktop/tags/drive/base-contextmenu.tag
rename to src/web/app/desktop/-tags/drive/base-contextmenu.tag
diff --git a/src/web/app/desktop/tags/drive/browser-window.tag b/src/web/app/desktop/-tags/drive/browser-window.tag
similarity index 100%
rename from src/web/app/desktop/tags/drive/browser-window.tag
rename to src/web/app/desktop/-tags/drive/browser-window.tag
diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/-tags/drive/browser.tag
similarity index 100%
rename from src/web/app/desktop/tags/drive/browser.tag
rename to src/web/app/desktop/-tags/drive/browser.tag
diff --git a/src/web/app/desktop/tags/drive/file-contextmenu.tag b/src/web/app/desktop/-tags/drive/file-contextmenu.tag
similarity index 100%
rename from src/web/app/desktop/tags/drive/file-contextmenu.tag
rename to src/web/app/desktop/-tags/drive/file-contextmenu.tag
diff --git a/src/web/app/desktop/tags/drive/file.tag b/src/web/app/desktop/-tags/drive/file.tag
similarity index 100%
rename from src/web/app/desktop/tags/drive/file.tag
rename to src/web/app/desktop/-tags/drive/file.tag
diff --git a/src/web/app/desktop/tags/drive/folder-contextmenu.tag b/src/web/app/desktop/-tags/drive/folder-contextmenu.tag
similarity index 100%
rename from src/web/app/desktop/tags/drive/folder-contextmenu.tag
rename to src/web/app/desktop/-tags/drive/folder-contextmenu.tag
diff --git a/src/web/app/desktop/tags/drive/folder.tag b/src/web/app/desktop/-tags/drive/folder.tag
similarity index 100%
rename from src/web/app/desktop/tags/drive/folder.tag
rename to src/web/app/desktop/-tags/drive/folder.tag
diff --git a/src/web/app/desktop/tags/drive/nav-folder.tag b/src/web/app/desktop/-tags/drive/nav-folder.tag
similarity index 100%
rename from src/web/app/desktop/tags/drive/nav-folder.tag
rename to src/web/app/desktop/-tags/drive/nav-folder.tag
diff --git a/src/web/app/desktop/tags/ellipsis-icon.tag b/src/web/app/desktop/-tags/ellipsis-icon.tag
similarity index 100%
rename from src/web/app/desktop/tags/ellipsis-icon.tag
rename to src/web/app/desktop/-tags/ellipsis-icon.tag
diff --git a/src/web/app/desktop/tags/follow-button.tag b/src/web/app/desktop/-tags/follow-button.tag
similarity index 100%
rename from src/web/app/desktop/tags/follow-button.tag
rename to src/web/app/desktop/-tags/follow-button.tag
diff --git a/src/web/app/desktop/tags/following-setuper.tag b/src/web/app/desktop/-tags/following-setuper.tag
similarity index 100%
rename from src/web/app/desktop/tags/following-setuper.tag
rename to src/web/app/desktop/-tags/following-setuper.tag
diff --git a/src/web/app/desktop/tags/home-widgets/access-log.tag b/src/web/app/desktop/-tags/home-widgets/access-log.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/access-log.tag
rename to src/web/app/desktop/-tags/home-widgets/access-log.tag
diff --git a/src/web/app/desktop/tags/home-widgets/activity.tag b/src/web/app/desktop/-tags/home-widgets/activity.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/activity.tag
rename to src/web/app/desktop/-tags/home-widgets/activity.tag
diff --git a/src/web/app/desktop/tags/home-widgets/broadcast.tag b/src/web/app/desktop/-tags/home-widgets/broadcast.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/broadcast.tag
rename to src/web/app/desktop/-tags/home-widgets/broadcast.tag
diff --git a/src/web/app/desktop/tags/home-widgets/calendar.tag b/src/web/app/desktop/-tags/home-widgets/calendar.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/calendar.tag
rename to src/web/app/desktop/-tags/home-widgets/calendar.tag
diff --git a/src/web/app/desktop/tags/home-widgets/channel.tag b/src/web/app/desktop/-tags/home-widgets/channel.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/channel.tag
rename to src/web/app/desktop/-tags/home-widgets/channel.tag
diff --git a/src/web/app/desktop/tags/home-widgets/donation.tag b/src/web/app/desktop/-tags/home-widgets/donation.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/donation.tag
rename to src/web/app/desktop/-tags/home-widgets/donation.tag
diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/-tags/home-widgets/mentions.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/mentions.tag
rename to src/web/app/desktop/-tags/home-widgets/mentions.tag
diff --git a/src/web/app/desktop/tags/home-widgets/messaging.tag b/src/web/app/desktop/-tags/home-widgets/messaging.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/messaging.tag
rename to src/web/app/desktop/-tags/home-widgets/messaging.tag
diff --git a/src/web/app/desktop/tags/home-widgets/nav.tag b/src/web/app/desktop/-tags/home-widgets/nav.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/nav.tag
rename to src/web/app/desktop/-tags/home-widgets/nav.tag
diff --git a/src/web/app/desktop/tags/home-widgets/notifications.tag b/src/web/app/desktop/-tags/home-widgets/notifications.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/notifications.tag
rename to src/web/app/desktop/-tags/home-widgets/notifications.tag
diff --git a/src/web/app/desktop/tags/home-widgets/photo-stream.tag b/src/web/app/desktop/-tags/home-widgets/photo-stream.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/photo-stream.tag
rename to src/web/app/desktop/-tags/home-widgets/photo-stream.tag
diff --git a/src/web/app/desktop/tags/home-widgets/post-form.tag b/src/web/app/desktop/-tags/home-widgets/post-form.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/post-form.tag
rename to src/web/app/desktop/-tags/home-widgets/post-form.tag
diff --git a/src/web/app/desktop/tags/home-widgets/profile.tag b/src/web/app/desktop/-tags/home-widgets/profile.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/profile.tag
rename to src/web/app/desktop/-tags/home-widgets/profile.tag
diff --git a/src/web/app/desktop/tags/home-widgets/recommended-polls.tag b/src/web/app/desktop/-tags/home-widgets/recommended-polls.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/recommended-polls.tag
rename to src/web/app/desktop/-tags/home-widgets/recommended-polls.tag
diff --git a/src/web/app/desktop/tags/home-widgets/rss-reader.tag b/src/web/app/desktop/-tags/home-widgets/rss-reader.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/rss-reader.tag
rename to src/web/app/desktop/-tags/home-widgets/rss-reader.tag
diff --git a/src/web/app/desktop/tags/home-widgets/server.tag b/src/web/app/desktop/-tags/home-widgets/server.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/server.tag
rename to src/web/app/desktop/-tags/home-widgets/server.tag
diff --git a/src/web/app/desktop/tags/home-widgets/slideshow.tag b/src/web/app/desktop/-tags/home-widgets/slideshow.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/slideshow.tag
rename to src/web/app/desktop/-tags/home-widgets/slideshow.tag
diff --git a/src/web/app/desktop/tags/home-widgets/timeline.tag b/src/web/app/desktop/-tags/home-widgets/timeline.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/timeline.tag
rename to src/web/app/desktop/-tags/home-widgets/timeline.tag
diff --git a/src/web/app/desktop/tags/home-widgets/timemachine.tag b/src/web/app/desktop/-tags/home-widgets/timemachine.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/timemachine.tag
rename to src/web/app/desktop/-tags/home-widgets/timemachine.tag
diff --git a/src/web/app/desktop/tags/home-widgets/tips.tag b/src/web/app/desktop/-tags/home-widgets/tips.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/tips.tag
rename to src/web/app/desktop/-tags/home-widgets/tips.tag
diff --git a/src/web/app/desktop/tags/home-widgets/trends.tag b/src/web/app/desktop/-tags/home-widgets/trends.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/trends.tag
rename to src/web/app/desktop/-tags/home-widgets/trends.tag
diff --git a/src/web/app/desktop/tags/home-widgets/user-recommendation.tag b/src/web/app/desktop/-tags/home-widgets/user-recommendation.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/user-recommendation.tag
rename to src/web/app/desktop/-tags/home-widgets/user-recommendation.tag
diff --git a/src/web/app/desktop/tags/home-widgets/version.tag b/src/web/app/desktop/-tags/home-widgets/version.tag
similarity index 100%
rename from src/web/app/desktop/tags/home-widgets/version.tag
rename to src/web/app/desktop/-tags/home-widgets/version.tag
diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/-tags/images.tag
similarity index 100%
rename from src/web/app/desktop/tags/images.tag
rename to src/web/app/desktop/-tags/images.tag
diff --git a/src/web/app/desktop/tags/index.ts b/src/web/app/desktop/-tags/index.ts
similarity index 100%
rename from src/web/app/desktop/tags/index.ts
rename to src/web/app/desktop/-tags/index.ts
diff --git a/src/web/app/desktop/tags/input-dialog.tag b/src/web/app/desktop/-tags/input-dialog.tag
similarity index 100%
rename from src/web/app/desktop/tags/input-dialog.tag
rename to src/web/app/desktop/-tags/input-dialog.tag
diff --git a/src/web/app/desktop/tags/list-user.tag b/src/web/app/desktop/-tags/list-user.tag
similarity index 100%
rename from src/web/app/desktop/tags/list-user.tag
rename to src/web/app/desktop/-tags/list-user.tag
diff --git a/src/web/app/desktop/tags/messaging/room-window.tag b/src/web/app/desktop/-tags/messaging/room-window.tag
similarity index 100%
rename from src/web/app/desktop/tags/messaging/room-window.tag
rename to src/web/app/desktop/-tags/messaging/room-window.tag
diff --git a/src/web/app/desktop/tags/messaging/window.tag b/src/web/app/desktop/-tags/messaging/window.tag
similarity index 100%
rename from src/web/app/desktop/tags/messaging/window.tag
rename to src/web/app/desktop/-tags/messaging/window.tag
diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/-tags/notifications.tag
similarity index 100%
rename from src/web/app/desktop/tags/notifications.tag
rename to src/web/app/desktop/-tags/notifications.tag
diff --git a/src/web/app/desktop/tags/pages/drive.tag b/src/web/app/desktop/-tags/pages/drive.tag
similarity index 100%
rename from src/web/app/desktop/tags/pages/drive.tag
rename to src/web/app/desktop/-tags/pages/drive.tag
diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/-tags/pages/entrance.tag
similarity index 100%
rename from src/web/app/desktop/tags/pages/entrance.tag
rename to src/web/app/desktop/-tags/pages/entrance.tag
diff --git a/src/web/app/desktop/tags/pages/home-customize.tag b/src/web/app/desktop/-tags/pages/home-customize.tag
similarity index 100%
rename from src/web/app/desktop/tags/pages/home-customize.tag
rename to src/web/app/desktop/-tags/pages/home-customize.tag
diff --git a/src/web/app/desktop/tags/pages/home.tag b/src/web/app/desktop/-tags/pages/home.tag
similarity index 100%
rename from src/web/app/desktop/tags/pages/home.tag
rename to src/web/app/desktop/-tags/pages/home.tag
diff --git a/src/web/app/desktop/tags/pages/messaging-room.tag b/src/web/app/desktop/-tags/pages/messaging-room.tag
similarity index 100%
rename from src/web/app/desktop/tags/pages/messaging-room.tag
rename to src/web/app/desktop/-tags/pages/messaging-room.tag
diff --git a/src/web/app/desktop/tags/pages/not-found.tag b/src/web/app/desktop/-tags/pages/not-found.tag
similarity index 100%
rename from src/web/app/desktop/tags/pages/not-found.tag
rename to src/web/app/desktop/-tags/pages/not-found.tag
diff --git a/src/web/app/desktop/tags/pages/post.tag b/src/web/app/desktop/-tags/pages/post.tag
similarity index 100%
rename from src/web/app/desktop/tags/pages/post.tag
rename to src/web/app/desktop/-tags/pages/post.tag
diff --git a/src/web/app/desktop/tags/pages/search.tag b/src/web/app/desktop/-tags/pages/search.tag
similarity index 100%
rename from src/web/app/desktop/tags/pages/search.tag
rename to src/web/app/desktop/-tags/pages/search.tag
diff --git a/src/web/app/desktop/tags/pages/selectdrive.tag b/src/web/app/desktop/-tags/pages/selectdrive.tag
similarity index 100%
rename from src/web/app/desktop/tags/pages/selectdrive.tag
rename to src/web/app/desktop/-tags/pages/selectdrive.tag
diff --git a/src/web/app/desktop/tags/pages/user.tag b/src/web/app/desktop/-tags/pages/user.tag
similarity index 100%
rename from src/web/app/desktop/tags/pages/user.tag
rename to src/web/app/desktop/-tags/pages/user.tag
diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/-tags/post-detail-sub.tag
similarity index 100%
rename from src/web/app/desktop/tags/post-detail-sub.tag
rename to src/web/app/desktop/-tags/post-detail-sub.tag
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/-tags/post-detail.tag
similarity index 100%
rename from src/web/app/desktop/tags/post-detail.tag
rename to src/web/app/desktop/-tags/post-detail.tag
diff --git a/src/web/app/desktop/tags/post-form-window.tag b/src/web/app/desktop/-tags/post-form-window.tag
similarity index 100%
rename from src/web/app/desktop/tags/post-form-window.tag
rename to src/web/app/desktop/-tags/post-form-window.tag
diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/-tags/post-form.tag
similarity index 100%
rename from src/web/app/desktop/tags/post-form.tag
rename to src/web/app/desktop/-tags/post-form.tag
diff --git a/src/web/app/desktop/tags/post-preview.tag b/src/web/app/desktop/-tags/post-preview.tag
similarity index 100%
rename from src/web/app/desktop/tags/post-preview.tag
rename to src/web/app/desktop/-tags/post-preview.tag
diff --git a/src/web/app/desktop/tags/progress-dialog.tag b/src/web/app/desktop/-tags/progress-dialog.tag
similarity index 100%
rename from src/web/app/desktop/tags/progress-dialog.tag
rename to src/web/app/desktop/-tags/progress-dialog.tag
diff --git a/src/web/app/desktop/tags/repost-form-window.tag b/src/web/app/desktop/-tags/repost-form-window.tag
similarity index 100%
rename from src/web/app/desktop/tags/repost-form-window.tag
rename to src/web/app/desktop/-tags/repost-form-window.tag
diff --git a/src/web/app/desktop/tags/repost-form.tag b/src/web/app/desktop/-tags/repost-form.tag
similarity index 100%
rename from src/web/app/desktop/tags/repost-form.tag
rename to src/web/app/desktop/-tags/repost-form.tag
diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/-tags/search-posts.tag
similarity index 100%
rename from src/web/app/desktop/tags/search-posts.tag
rename to src/web/app/desktop/-tags/search-posts.tag
diff --git a/src/web/app/desktop/tags/search.tag b/src/web/app/desktop/-tags/search.tag
similarity index 100%
rename from src/web/app/desktop/tags/search.tag
rename to src/web/app/desktop/-tags/search.tag
diff --git a/src/web/app/desktop/tags/select-file-from-drive-window.tag b/src/web/app/desktop/-tags/select-file-from-drive-window.tag
similarity index 100%
rename from src/web/app/desktop/tags/select-file-from-drive-window.tag
rename to src/web/app/desktop/-tags/select-file-from-drive-window.tag
diff --git a/src/web/app/desktop/tags/select-folder-from-drive-window.tag b/src/web/app/desktop/-tags/select-folder-from-drive-window.tag
similarity index 100%
rename from src/web/app/desktop/tags/select-folder-from-drive-window.tag
rename to src/web/app/desktop/-tags/select-folder-from-drive-window.tag
diff --git a/src/web/app/desktop/tags/set-avatar-suggestion.tag b/src/web/app/desktop/-tags/set-avatar-suggestion.tag
similarity index 100%
rename from src/web/app/desktop/tags/set-avatar-suggestion.tag
rename to src/web/app/desktop/-tags/set-avatar-suggestion.tag
diff --git a/src/web/app/desktop/tags/set-banner-suggestion.tag b/src/web/app/desktop/-tags/set-banner-suggestion.tag
similarity index 100%
rename from src/web/app/desktop/tags/set-banner-suggestion.tag
rename to src/web/app/desktop/-tags/set-banner-suggestion.tag
diff --git a/src/web/app/desktop/tags/settings-window.tag b/src/web/app/desktop/-tags/settings-window.tag
similarity index 100%
rename from src/web/app/desktop/tags/settings-window.tag
rename to src/web/app/desktop/-tags/settings-window.tag
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/-tags/settings.tag
similarity index 100%
rename from src/web/app/desktop/tags/settings.tag
rename to src/web/app/desktop/-tags/settings.tag
diff --git a/src/web/app/desktop/tags/sub-post-content.tag b/src/web/app/desktop/-tags/sub-post-content.tag
similarity index 100%
rename from src/web/app/desktop/tags/sub-post-content.tag
rename to src/web/app/desktop/-tags/sub-post-content.tag
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/-tags/timeline.tag
similarity index 100%
rename from src/web/app/desktop/tags/timeline.tag
rename to src/web/app/desktop/-tags/timeline.tag
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/-tags/ui.tag
similarity index 100%
rename from src/web/app/desktop/tags/ui.tag
rename to src/web/app/desktop/-tags/ui.tag
diff --git a/src/web/app/desktop/tags/user-followers-window.tag b/src/web/app/desktop/-tags/user-followers-window.tag
similarity index 100%
rename from src/web/app/desktop/tags/user-followers-window.tag
rename to src/web/app/desktop/-tags/user-followers-window.tag
diff --git a/src/web/app/desktop/tags/user-followers.tag b/src/web/app/desktop/-tags/user-followers.tag
similarity index 100%
rename from src/web/app/desktop/tags/user-followers.tag
rename to src/web/app/desktop/-tags/user-followers.tag
diff --git a/src/web/app/desktop/tags/user-following-window.tag b/src/web/app/desktop/-tags/user-following-window.tag
similarity index 100%
rename from src/web/app/desktop/tags/user-following-window.tag
rename to src/web/app/desktop/-tags/user-following-window.tag
diff --git a/src/web/app/desktop/tags/user-following.tag b/src/web/app/desktop/-tags/user-following.tag
similarity index 100%
rename from src/web/app/desktop/tags/user-following.tag
rename to src/web/app/desktop/-tags/user-following.tag
diff --git a/src/web/app/desktop/tags/user-preview.tag b/src/web/app/desktop/-tags/user-preview.tag
similarity index 100%
rename from src/web/app/desktop/tags/user-preview.tag
rename to src/web/app/desktop/-tags/user-preview.tag
diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/-tags/user-timeline.tag
similarity index 100%
rename from src/web/app/desktop/tags/user-timeline.tag
rename to src/web/app/desktop/-tags/user-timeline.tag
diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/-tags/user.tag
similarity index 100%
rename from src/web/app/desktop/tags/user.tag
rename to src/web/app/desktop/-tags/user.tag
diff --git a/src/web/app/desktop/tags/users-list.tag b/src/web/app/desktop/-tags/users-list.tag
similarity index 100%
rename from src/web/app/desktop/tags/users-list.tag
rename to src/web/app/desktop/-tags/users-list.tag
diff --git a/src/web/app/desktop/tags/widgets/activity.tag b/src/web/app/desktop/-tags/widgets/activity.tag
similarity index 100%
rename from src/web/app/desktop/tags/widgets/activity.tag
rename to src/web/app/desktop/-tags/widgets/activity.tag
diff --git a/src/web/app/desktop/tags/widgets/calendar.tag b/src/web/app/desktop/-tags/widgets/calendar.tag
similarity index 100%
rename from src/web/app/desktop/tags/widgets/calendar.tag
rename to src/web/app/desktop/-tags/widgets/calendar.tag
diff --git a/src/web/app/desktop/tags/window.tag b/src/web/app/desktop/-tags/window.tag
similarity index 100%
rename from src/web/app/desktop/tags/window.tag
rename to src/web/app/desktop/-tags/window.tag

From 24478a060ef7d1668b6055b494421118b8c1fed1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Feb 2018 13:32:41 +0900
Subject: [PATCH 021/286] wip

---
 package.json        | 2 ++
 src/web/app/init.ts | 7 +++++--
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index bd5114480..33a0b2e2d 100644
--- a/package.json
+++ b/package.json
@@ -172,6 +172,8 @@
 		"uglifyjs-webpack-plugin": "1.1.8",
 		"uuid": "3.2.1",
 		"vhost": "3.0.2",
+		"vue": "^2.5.13",
+		"vue-router": "^3.0.1",
 		"web-push": "3.2.5",
 		"webpack": "3.10.0",
 		"websocket": "1.0.25",
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 154b1ba0f..62bd6949b 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -7,11 +7,14 @@ declare const _LANG_: string;
 declare const _HOST_: string;
 declare const __CONSTS__: any;
 
-import * as riot from 'riot';
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+
+Vue.use(VueRouter);
+
 import checkForUpdate from './common/scripts/check-for-update';
 import mixin from './common/mixins';
 import MiOS from './common/mios';
-require('./common/tags');
 
 /**
  * APP ENTRY POINT!

From b7596e357c373b95d681bbf3f0b12683fa467e71 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Feb 2018 18:28:06 +0900
Subject: [PATCH 022/286] wip

---
 src/tsconfig.json                |  4 ++-
 src/web/app/common/mios.ts       | 35 ++++++++++++++++----
 src/web/app/common/mixins.ts     | 40 -----------------------
 src/web/app/common/tags/time.vue | 56 ++++++++++++++++++--------------
 src/web/app/init.ts              | 24 +++++++-------
 tsconfig.json                    |  4 ++-
 6 files changed, 79 insertions(+), 84 deletions(-)
 delete mode 100644 src/web/app/common/mixins.ts

diff --git a/src/tsconfig.json b/src/tsconfig.json
index 36600eed2..d88432d24 100644
--- a/src/tsconfig.json
+++ b/src/tsconfig.json
@@ -12,7 +12,9 @@
     "target": "es2017",
     "module": "commonjs",
     "removeComments": false,
-    "noLib": false
+    "noLib": false,
+    "strict": true,
+    "strictNullChecks": false
   },
   "compileOnSave": false,
   "include": [
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index 6ee42ea8a..b947e0743 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -4,6 +4,10 @@ import signout from './scripts/signout';
 import Progress from './scripts/loading';
 import HomeStreamManager from './scripts/streaming/home-stream-manager';
 import api from './scripts/api';
+import DriveStreamManager from './scripts/streaming/drive-stream-manager';
+import ServerStreamManager from './scripts/streaming/server-stream-manager';
+import RequestsStreamManager from './scripts/streaming/requests-stream-manager';
+import MessagingIndexStreamManager from './scripts/streaming/messaging-index-stream-manager';
 
 //#region environment variables
 declare const _VERSION_: string;
@@ -50,6 +54,16 @@ export default class MiOS extends EventEmitter {
 	 */
 	public stream: HomeStreamManager;
 
+	/**
+	 * Connection managers
+	 */
+	public streams: {
+		driveStream: DriveStreamManager;
+		serverStream: ServerStreamManager;
+		requestsStream: RequestsStreamManager;
+		messagingIndexStream: MessagingIndexStreamManager;
+	};
+
 	/**
 	 * A registration of service worker
 	 */
@@ -69,6 +83,9 @@ export default class MiOS extends EventEmitter {
 
 		this.shouldRegisterSw = shouldRegisterSw;
 
+		this.streams.serverStream = new ServerStreamManager();
+		this.streams.requestsStream = new RequestsStreamManager();
+
 		//#region BIND
 		this.log = this.log.bind(this);
 		this.logInfo = this.logInfo.bind(this);
@@ -79,6 +96,15 @@ export default class MiOS extends EventEmitter {
 		this.getMeta = this.getMeta.bind(this);
 		this.registerSw = this.registerSw.bind(this);
 		//#endregion
+
+		this.once('signedin', () => {
+			// Init home stream manager
+			this.stream = new HomeStreamManager(this.i);
+
+			// Init other stream manager
+			this.streams.driveStream = new DriveStreamManager(this.i);
+			this.streams.messagingIndexStream = new MessagingIndexStreamManager(this.i);
+		});
 	}
 
 	public log(...args) {
@@ -139,8 +165,8 @@ export default class MiOS extends EventEmitter {
 			// When failure
 			.catch(() => {
 				// Render the error screen
-				document.body.innerHTML = '<mk-error />';
-				riot.mount('*');
+				//document.body.innerHTML = '<mk-error />';
+				//riot.mount('*');
 
 				Progress.done();
 			});
@@ -173,10 +199,7 @@ export default class MiOS extends EventEmitter {
 
 			this.i = me;
 
-			// Init home stream manager
-			this.stream = this.isSignedin
-				? new HomeStreamManager(this.i)
-				: null;
+			this.emit('signedin');
 
 			// Finish init
 			callback();
diff --git a/src/web/app/common/mixins.ts b/src/web/app/common/mixins.ts
deleted file mode 100644
index e9c362593..000000000
--- a/src/web/app/common/mixins.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import * as riot from 'riot';
-
-import MiOS from './mios';
-import ServerStreamManager from './scripts/streaming/server-stream-manager';
-import RequestsStreamManager from './scripts/streaming/requests-stream-manager';
-import MessagingIndexStreamManager from './scripts/streaming/messaging-index-stream-manager';
-import DriveStreamManager from './scripts/streaming/drive-stream-manager';
-
-export default (mios: MiOS) => {
-	(riot as any).mixin('os', {
-		mios: mios
-	});
-
-	(riot as any).mixin('i', {
-		init: function() {
-			this.I = mios.i;
-			this.SIGNIN = mios.isSignedin;
-
-			if (this.SIGNIN) {
-				this.on('mount', () => {
-					mios.i.on('updated', this.update);
-				});
-				this.on('unmount', () => {
-					mios.i.off('updated', this.update);
-				});
-			}
-		},
-		me: mios.i
-	});
-
-	(riot as any).mixin('api', {
-		api: mios.api
-	});
-
-	(riot as any).mixin('stream', { stream: mios.stream });
-	(riot as any).mixin('drive-stream', { driveStream: new DriveStreamManager(mios.i) });
-	(riot as any).mixin('server-stream', { serverStream: new ServerStreamManager() });
-	(riot as any).mixin('requests-stream', { requestsStream: new RequestsStreamManager() });
-	(riot as any).mixin('messaging-index-stream', { messagingIndexStream: new MessagingIndexStreamManager(mios.i) });
-};
diff --git a/src/web/app/common/tags/time.vue b/src/web/app/common/tags/time.vue
index 0239f5422..7d165fc00 100644
--- a/src/web/app/common/tags/time.vue
+++ b/src/web/app/common/tags/time.vue
@@ -7,23 +7,43 @@
 </template>
 
 <script lang="typescript">
-	export default {
+	import Vue from 'vue';
+
+	export default Vue.extend({
 		props: ['time', 'mode'],
 		data() {
 			return {
 				mode: 'relative',
-				tickId: null
+				tickId: null,
+				now: new Date()
 			};
 		},
+		computed: {
+			absolute() {
+				return (
+					this.time.getFullYear()    + '年' +
+					(this.time.getMonth() + 1) + '月' +
+					this.time.getDate()        + '日' +
+					' ' +
+					this.time.getHours()       + '時' +
+					this.time.getMinutes()     + '分');
+			},
+			relative() {
+				const ago = (this.now - this.time) / 1000/*ms*/;
+				return (
+					ago >= 31536000 ? '%i18n:common.time.years_ago%'  .replace('{}', ~~(ago / 31536000)) :
+					ago >= 2592000  ? '%i18n:common.time.months_ago%' .replace('{}', ~~(ago / 2592000)) :
+					ago >= 604800   ? '%i18n:common.time.weeks_ago%'  .replace('{}', ~~(ago / 604800)) :
+					ago >= 86400    ? '%i18n:common.time.days_ago%'   .replace('{}', ~~(ago / 86400)) :
+					ago >= 3600     ? '%i18n:common.time.hours_ago%'  .replace('{}', ~~(ago / 3600)) :
+					ago >= 60       ? '%i18n:common.time.minutes_ago%'.replace('{}', ~~(ago / 60)) :
+					ago >= 10       ? '%i18n:common.time.seconds_ago%'.replace('{}', ~~(ago % 60)) :
+					ago >= 0        ? '%i18n:common.time.just_now%' :
+					ago <  0        ? '%i18n:common.time.future%' :
+					'%i18n:common.time.unknown%');
+			}
+		},
 		created() {
-			this.absolute =
-				this.time.getFullYear()    + '年' +
-				(this.time.getMonth() + 1) + '月' +
-				this.time.getDate()        + '日' +
-				' ' +
-				this.time.getHours()       + '時' +
-				this.time.getMinutes()     + '分';
-
 			if (this.mode == 'relative' || this.mode == 'detail') {
 				this.tick();
 				this.tickId = setInterval(this.tick, 1000);
@@ -36,20 +56,8 @@
 		},
 		methods: {
 			tick() {
-				const now = new Date();
-				const ago = (now - this.time) / 1000/*ms*/;
-				this.relative =
-					ago >= 31536000 ? '%i18n:common.time.years_ago%'  .replace('{}', ~~(ago / 31536000)) :
-					ago >= 2592000  ? '%i18n:common.time.months_ago%' .replace('{}', ~~(ago / 2592000)) :
-					ago >= 604800   ? '%i18n:common.time.weeks_ago%'  .replace('{}', ~~(ago / 604800)) :
-					ago >= 86400    ? '%i18n:common.time.days_ago%'   .replace('{}', ~~(ago / 86400)) :
-					ago >= 3600     ? '%i18n:common.time.hours_ago%'  .replace('{}', ~~(ago / 3600)) :
-					ago >= 60       ? '%i18n:common.time.minutes_ago%'.replace('{}', ~~(ago / 60)) :
-					ago >= 10       ? '%i18n:common.time.seconds_ago%'.replace('{}', ~~(ago % 60)) :
-					ago >= 0        ? '%i18n:common.time.just_now%' :
-					ago <  0        ? '%i18n:common.time.future%' :
-					'%i18n:common.time.unknown%';
+				this.now = new Date();
 			}
 		}
-	};
+	});
 </script>
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 62bd6949b..4b2a3b868 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -30,21 +30,21 @@ if (_HOST_ != 'localhost') {
 	document.domain = _HOST_;
 }
 
-{ // Set lang attr
-	const html = document.documentElement;
-	html.setAttribute('lang', _LANG_);
-}
+//#region Set lang attr
+const html = document.documentElement;
+html.setAttribute('lang', _LANG_);
+//#endregion
 
-{ // Set description meta tag
-	const head = document.getElementsByTagName('head')[0];
-	const meta = document.createElement('meta');
-	meta.setAttribute('name', 'description');
-	meta.setAttribute('content', '%i18n:common.misskey%');
-	head.appendChild(meta);
-}
+//#region Set description meta tag
+const head = document.getElementsByTagName('head')[0];
+const meta = document.createElement('meta');
+meta.setAttribute('name', 'description');
+meta.setAttribute('content', '%i18n:common.misskey%');
+head.appendChild(meta);
+//#endregion
 
 // Set global configuration
-(riot as any).mixin(__CONSTS__);
+//(riot as any).mixin(__CONSTS__);
 
 // iOSでプライベートモードだとlocalStorageが使えないので既存のメソッドを上書きする
 try {
diff --git a/tsconfig.json b/tsconfig.json
index a38ff220b..68f6809b9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -12,7 +12,9 @@
     "target": "es2017",
     "module": "commonjs",
     "removeComments": false,
-    "noLib": false
+    "noLib": false,
+    "strict": true,
+    "strictNullChecks": false
   },
   "compileOnSave": false,
   "include": [

From d5f345c8f97a9e785884aa1f3b17696472e026cf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 9 Feb 2018 18:57:42 +0900
Subject: [PATCH 023/286] wip

---
 src/web/app/desktop/script.ts |  6 ++----
 src/web/app/init.ts           | 18 ++++++------------
 2 files changed, 8 insertions(+), 16 deletions(-)

diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index b06cb180e..2d3714d84 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -5,9 +5,7 @@
 // Style
 import './style.styl';
 
-require('./tags');
-require('./mixins');
-import * as riot from 'riot';
+import Vue from 'vue';
 import init from '../init';
 import route from './router';
 import fuckAdBlock from './scripts/fuck-ad-block';
@@ -18,7 +16,7 @@ import composeNotification from '../common/scripts/compose-notification';
 /**
  * init
  */
-init(async (mios: MiOS) => {
+init(async (mios: MiOS, app: Vue) => {
 	/**
 	 * Fuck AD Block
 	 */
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 4b2a3b868..5fb6ae790 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -13,7 +13,6 @@ import VueRouter from 'vue-router';
 Vue.use(VueRouter);
 
 import checkForUpdate from './common/scripts/check-for-update';
-import mixin from './common/mixins';
 import MiOS from './common/mios';
 
 /**
@@ -64,20 +63,15 @@ export default (callback, sw = false) => {
 	const mios = new MiOS(sw);
 
 	mios.init(() => {
-		// ミックスイン初期化
-		mixin(mios);
-
-		// ローディング画面クリア
-		const ini = document.getElementById('ini');
-		ini.parentNode.removeChild(ini);
-
 		// アプリ基底要素マウント
-		const app = document.createElement('div');
-		app.setAttribute('id', 'app');
-		document.body.appendChild(app);
+		document.body.innerHTML = '<div id="app"><router-view></router-view></div>';
+
+		const app = new Vue({
+			router: new VueRouter()
+		}).$mount('#app');
 
 		try {
-			callback(mios);
+			callback(mios, app);
 		} catch (e) {
 			panic(e);
 		}

From ff7bb97d8ee04a6a56aaea8a09f9b4d7170f2064 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 10:27:05 +0900
Subject: [PATCH 024/286] wip

---
 gulpfile.ts                                 |  2 +-
 package.json                                |  4 +-
 src/api/bot/core.ts                         |  4 +-
 src/common/build/i18n.ts                    | 13 +++-
 src/web/app/desktop/mixins/index.ts         |  2 -
 src/web/app/desktop/mixins/user-preview.ts  | 66 ---------------------
 src/web/app/desktop/mixins/widget.ts        | 31 ----------
 src/web/app/desktop/script.ts               | 12 ++--
 src/web/app/desktop/scripts/autocomplete.ts |  2 +-
 src/web/app/desktop/tags/pages/index.vue    |  3 +
 src/web/app/init.ts                         |  2 +-
 src/{ => web/app}/tsconfig.json             |  3 -
 src/web/app/v.d.ts                          |  4 ++
 tsconfig.json                               |  5 +-
 webpack/module/rules/index.ts               |  4 +-
 webpack/module/rules/tag.ts                 | 20 -------
 webpack/module/rules/typescript.ts          |  6 +-
 webpack/module/rules/vue.ts                 |  9 +++
 webpack/webpack.config.ts                   | 12 ++--
 19 files changed, 58 insertions(+), 146 deletions(-)
 delete mode 100644 src/web/app/desktop/mixins/index.ts
 delete mode 100644 src/web/app/desktop/mixins/user-preview.ts
 delete mode 100644 src/web/app/desktop/mixins/widget.ts
 create mode 100644 src/web/app/desktop/tags/pages/index.vue
 rename src/{ => web/app}/tsconfig.json (91%)
 create mode 100644 src/web/app/v.d.ts
 delete mode 100644 webpack/module/rules/tag.ts
 create mode 100644 webpack/module/rules/vue.ts

diff --git a/gulpfile.ts b/gulpfile.ts
index 21870473e..736507baf 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -56,7 +56,7 @@ gulp.task('build:js', () =>
 );
 
 gulp.task('build:ts', () => {
-	const tsProject = ts.createProject('./src/tsconfig.json');
+	const tsProject = ts.createProject('./tsconfig.json');
 
 	return tsProject
 		.src()
diff --git a/package.json b/package.json
index 33a0b2e2d..56501266b 100644
--- a/package.json
+++ b/package.json
@@ -81,7 +81,6 @@
 		"accesses": "2.5.0",
 		"animejs": "2.2.0",
 		"autwh": "0.0.1",
-		"awesome-typescript-loader": "3.4.1",
 		"bcryptjs": "2.4.3",
 		"body-parser": "1.18.2",
 		"cafy": "3.2.1",
@@ -165,6 +164,7 @@
 		"tcp-port-used": "0.1.2",
 		"textarea-caret": "3.0.2",
 		"tmp": "0.0.33",
+		"ts-loader": "^3.5.0",
 		"ts-node": "4.1.0",
 		"tslint": "5.9.1",
 		"typescript": "2.7.1",
@@ -173,7 +173,9 @@
 		"uuid": "3.2.1",
 		"vhost": "3.0.2",
 		"vue": "^2.5.13",
+		"vue-loader": "^14.1.1",
 		"vue-router": "^3.0.1",
+		"vue-template-compiler": "^2.5.13",
 		"web-push": "3.2.5",
 		"webpack": "3.10.0",
 		"websocket": "1.0.25",
diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index ddae6405f..0a073a312 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -305,7 +305,7 @@ class TlContext extends Context {
 	private async getTl() {
 		const tl = await require('../endpoints/posts/timeline')({
 			limit: 5,
-			max_id: this.next ? this.next : undefined
+			until_id: this.next ? this.next : undefined
 		}, this.bot.user);
 
 		if (tl.length > 0) {
@@ -357,7 +357,7 @@ class NotificationsContext extends Context {
 	private async getNotifications() {
 		const notifications = await require('../endpoints/i/notifications')({
 			limit: 5,
-			max_id: this.next ? this.next : undefined
+			until_id: this.next ? this.next : undefined
 		}, this.bot.user);
 
 		if (notifications.length > 0) {
diff --git a/src/common/build/i18n.ts b/src/common/build/i18n.ts
index 1ae22147c..500b8814f 100644
--- a/src/common/build/i18n.ts
+++ b/src/common/build/i18n.ts
@@ -17,12 +17,19 @@ export default class Replacer {
 	}
 
 	private get(key: string) {
-		let text = locale[this.lang];
+		const texts = locale[this.lang];
+
+		if (texts == null) {
+			console.warn(`lang '${this.lang}' is not supported`);
+			return key; // Fallback
+		}
+
+		let text;
 
 		// Check the key existance
 		const error = key.split('.').some(k => {
-			if (text.hasOwnProperty(k)) {
-				text = text[k];
+			if (texts.hasOwnProperty(k)) {
+				text = texts[k];
 				return false;
 			} else {
 				return true;
diff --git a/src/web/app/desktop/mixins/index.ts b/src/web/app/desktop/mixins/index.ts
deleted file mode 100644
index e0c94ec5e..000000000
--- a/src/web/app/desktop/mixins/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-require('./user-preview');
-require('./widget');
diff --git a/src/web/app/desktop/mixins/user-preview.ts b/src/web/app/desktop/mixins/user-preview.ts
deleted file mode 100644
index 614de72be..000000000
--- a/src/web/app/desktop/mixins/user-preview.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import * as riot from 'riot';
-
-riot.mixin('user-preview', {
-	init: function() {
-		const scan = () => {
-			this.root.querySelectorAll('[data-user-preview]:not([data-user-preview-attached])')
-				.forEach(attach.bind(this));
-		};
-		this.on('mount', scan);
-		this.on('updated', scan);
-	}
-});
-
-function attach(el) {
-	el.setAttribute('data-user-preview-attached', true);
-
-	const user = el.getAttribute('data-user-preview');
-	let tag = null;
-	let showTimer = null;
-	let hideTimer = null;
-
-	el.addEventListener('mouseover', () => {
-		clearTimeout(showTimer);
-		clearTimeout(hideTimer);
-		showTimer = setTimeout(show, 500);
-	});
-
-	el.addEventListener('mouseleave', () => {
-		clearTimeout(showTimer);
-		clearTimeout(hideTimer);
-		hideTimer = setTimeout(close, 500);
-	});
-
-	this.on('unmount', () => {
-		clearTimeout(showTimer);
-		clearTimeout(hideTimer);
-		close();
-	});
-
-	const show = () => {
-		if (tag) return;
-		const preview = document.createElement('mk-user-preview');
-		const rect = el.getBoundingClientRect();
-		const x = rect.left + el.offsetWidth + window.pageXOffset;
-		const y = rect.top + window.pageYOffset;
-		preview.style.top = y + 'px';
-		preview.style.left = x + 'px';
-		preview.addEventListener('mouseover', () => {
-			clearTimeout(hideTimer);
-		});
-		preview.addEventListener('mouseleave', () => {
-			clearTimeout(showTimer);
-			hideTimer = setTimeout(close, 500);
-		});
-		tag = (riot as any).mount(document.body.appendChild(preview), {
-			user: user
-		})[0];
-	};
-
-	const close = () => {
-		if (tag) {
-			tag.close();
-			tag = null;
-		}
-	};
-}
diff --git a/src/web/app/desktop/mixins/widget.ts b/src/web/app/desktop/mixins/widget.ts
deleted file mode 100644
index 04131cd8f..000000000
--- a/src/web/app/desktop/mixins/widget.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import * as riot from 'riot';
-
-// ミックスインにオプションを渡せないのアレ
-// SEE: https://github.com/riot/riot/issues/2434
-
-(riot as any).mixin('widget', {
-	init: function() {
-		this.mixin('i');
-		this.mixin('api');
-
-		this.id = this.opts.id;
-		this.place = this.opts.place;
-
-		if (this.data) {
-			Object.keys(this.data).forEach(prop => {
-				this.data[prop] = this.opts.data.hasOwnProperty(prop) ? this.opts.data[prop] : this.data[prop];
-			});
-		}
-	},
-
-	save: function() {
-		this.update();
-		this.api('i/update_home', {
-			id: this.id,
-			data: this.data
-		}).then(() => {
-			this.I.client_settings.home.find(w => w.id == this.id).data = this.data;
-			this.I.update();
-		});
-	}
-});
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index 2d3714d84..4aef69b07 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -7,12 +7,13 @@ import './style.styl';
 
 import Vue from 'vue';
 import init from '../init';
-import route from './router';
 import fuckAdBlock from './scripts/fuck-ad-block';
 import MiOS from '../common/mios';
 import HomeStreamManager from '../common/scripts/streaming/home-stream-manager';
 import composeNotification from '../common/scripts/compose-notification';
 
+import MkIndex from './tags/pages/index.vue';
+
 /**
  * init
  */
@@ -36,8 +37,9 @@ init(async (mios: MiOS, app: Vue) => {
 		}
 	}
 
-	// Start routing
-	route(mios);
+	app.$router.addRoutes([{
+		path: '/', component: MkIndex, props: { os: mios }
+	}]);
 }, true);
 
 function registerNotifications(stream: HomeStreamManager) {
@@ -96,9 +98,9 @@ function registerNotifications(stream: HomeStreamManager) {
 			});
 			n.onclick = () => {
 				n.close();
-				(riot as any).mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
+				/*(riot as any).mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
 					user: message.user
-				});
+				});*/
 			};
 			setTimeout(n.close.bind(n), 7000);
 		});
diff --git a/src/web/app/desktop/scripts/autocomplete.ts b/src/web/app/desktop/scripts/autocomplete.ts
index 9df7aae08..8f075efdd 100644
--- a/src/web/app/desktop/scripts/autocomplete.ts
+++ b/src/web/app/desktop/scripts/autocomplete.ts
@@ -1,4 +1,4 @@
-import getCaretCoordinates = require('textarea-caret');
+import getCaretCoordinates from 'textarea-caret';
 import * as riot from 'riot';
 
 /**
diff --git a/src/web/app/desktop/tags/pages/index.vue b/src/web/app/desktop/tags/pages/index.vue
new file mode 100644
index 000000000..6bd036fc2
--- /dev/null
+++ b/src/web/app/desktop/tags/pages/index.vue
@@ -0,0 +1,3 @@
+<template>
+	<h1>hi</h1>
+</template>
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 5fb6ae790..f0c36f6c1 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -5,7 +5,7 @@
 declare const _VERSION_: string;
 declare const _LANG_: string;
 declare const _HOST_: string;
-declare const __CONSTS__: any;
+//declare const __CONSTS__: any;
 
 import Vue from 'vue';
 import VueRouter from 'vue-router';
diff --git a/src/tsconfig.json b/src/web/app/tsconfig.json
similarity index 91%
rename from src/tsconfig.json
rename to src/web/app/tsconfig.json
index d88432d24..e31b52dab 100644
--- a/src/tsconfig.json
+++ b/src/web/app/tsconfig.json
@@ -19,8 +19,5 @@
   "compileOnSave": false,
   "include": [
     "./**/*.ts"
-  ],
-  "exclude": [
-    "./web/app/**/*.ts"
   ]
 }
diff --git a/src/web/app/v.d.ts b/src/web/app/v.d.ts
new file mode 100644
index 000000000..8f3a240d8
--- /dev/null
+++ b/src/web/app/v.d.ts
@@ -0,0 +1,4 @@
+declare module "*.vue" {
+	import Vue from 'vue';
+	export default Vue;
+}
diff --git a/tsconfig.json b/tsconfig.json
index 68f6809b9..9d26429c5 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -18,6 +18,9 @@
   },
   "compileOnSave": false,
   "include": [
-    "./gulpfile.ts"
+    "./src/**/*.ts"
+  ],
+  "exclude": [
+    "./src/web/app/**/*.ts"
   ]
 }
diff --git a/webpack/module/rules/index.ts b/webpack/module/rules/index.ts
index b02bdef72..093f07330 100644
--- a/webpack/module/rules/index.ts
+++ b/webpack/module/rules/index.ts
@@ -3,7 +3,7 @@ import license from './license';
 import fa from './fa';
 import base64 from './base64';
 import themeColor from './theme-color';
-import tag from './tag';
+import vue from './vue';
 import stylus from './stylus';
 import typescript from './typescript';
 
@@ -13,7 +13,7 @@ export default lang => [
 	fa(),
 	base64(),
 	themeColor(),
-	tag(),
+	vue(),
 	stylus(),
 	typescript()
 ];
diff --git a/webpack/module/rules/tag.ts b/webpack/module/rules/tag.ts
deleted file mode 100644
index 706af35b4..000000000
--- a/webpack/module/rules/tag.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Riot tags
- */
-
-export default () => ({
-	test: /\.tag$/,
-	exclude: /node_modules/,
-	loader: 'riot-tag-loader',
-	query: {
-		hot: false,
-		style: 'stylus',
-		expr: false,
-		compact: true,
-		parserOptions: {
-			style: {
-				compress: true
-			}
-		}
-	}
-});
diff --git a/webpack/module/rules/typescript.ts b/webpack/module/rules/typescript.ts
index eb2b279a5..2c9413731 100644
--- a/webpack/module/rules/typescript.ts
+++ b/webpack/module/rules/typescript.ts
@@ -4,5 +4,9 @@
 
 export default () => ({
 	test: /\.ts$/,
-	use: 'awesome-typescript-loader'
+	loader: 'ts-loader',
+	options: {
+		configFile: __dirname + '/../../../src/web/app/tsconfig.json',
+		appendTsSuffixTo: [/\.vue$/]
+	}
 });
diff --git a/webpack/module/rules/vue.ts b/webpack/module/rules/vue.ts
new file mode 100644
index 000000000..0d38b4deb
--- /dev/null
+++ b/webpack/module/rules/vue.ts
@@ -0,0 +1,9 @@
+/**
+ * Vue
+ */
+
+export default () => ({
+	test: /\.vue$/,
+	exclude: /node_modules/,
+	loader: 'vue-loader'
+});
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index d67b8ef77..4386de3db 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -15,12 +15,12 @@ module.exports = Object.keys(langs).map(lang => {
 	// Entries
 	const entry = {
 		desktop: './src/web/app/desktop/script.ts',
-		mobile: './src/web/app/mobile/script.ts',
-		ch: './src/web/app/ch/script.ts',
-		stats: './src/web/app/stats/script.ts',
-		status: './src/web/app/status/script.ts',
-		dev: './src/web/app/dev/script.ts',
-		auth: './src/web/app/auth/script.ts',
+		//mobile: './src/web/app/mobile/script.ts',
+		//ch: './src/web/app/ch/script.ts',
+		//stats: './src/web/app/stats/script.ts',
+		//status: './src/web/app/status/script.ts',
+		//dev: './src/web/app/dev/script.ts',
+		//auth: './src/web/app/auth/script.ts',
 		sw: './src/web/app/sw.js'
 	};
 

From f339d028d26b5218a77e7b411d98c2dc60c853b4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 10:32:59 +0900
Subject: [PATCH 025/286] wip

---
 src/common/build/i18n.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/common/build/i18n.ts b/src/common/build/i18n.ts
index 500b8814f..5e3c0381a 100644
--- a/src/common/build/i18n.ts
+++ b/src/common/build/i18n.ts
@@ -24,12 +24,12 @@ export default class Replacer {
 			return key; // Fallback
 		}
 
-		let text;
+		let text = texts;
 
 		// Check the key existance
 		const error = key.split('.').some(k => {
-			if (texts.hasOwnProperty(k)) {
-				text = texts[k];
+			if (text.hasOwnProperty(k)) {
+				text = text[k];
 				return false;
 			} else {
 				return true;

From 29b1541a8e8506cd211026d5cf982a1e66e38dd6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 10:52:26 +0900
Subject: [PATCH 026/286] wip

---
 src/web/app/app.vue        | 3 +++
 src/web/app/common/mios.ts | 5 +++++
 src/web/app/init.ts        | 9 +++++++--
 3 files changed, 15 insertions(+), 2 deletions(-)
 create mode 100644 src/web/app/app.vue

diff --git a/src/web/app/app.vue b/src/web/app/app.vue
new file mode 100644
index 000000000..497d47003
--- /dev/null
+++ b/src/web/app/app.vue
@@ -0,0 +1,3 @@
+<template>
+	<router-view></router-view>
+</template>
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index b947e0743..4ff2333e8 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -62,6 +62,11 @@ export default class MiOS extends EventEmitter {
 		serverStream: ServerStreamManager;
 		requestsStream: RequestsStreamManager;
 		messagingIndexStream: MessagingIndexStreamManager;
+	} = {
+		driveStream: null,
+		serverStream: null,
+		requestsStream: null,
+		messagingIndexStream: null
 	};
 
 	/**
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index f0c36f6c1..91797a95a 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -12,6 +12,8 @@ import VueRouter from 'vue-router';
 
 Vue.use(VueRouter);
 
+import App from './app.vue';
+
 import checkForUpdate from './common/scripts/check-for-update';
 import MiOS from './common/mios';
 
@@ -64,10 +66,13 @@ export default (callback, sw = false) => {
 
 	mios.init(() => {
 		// アプリ基底要素マウント
-		document.body.innerHTML = '<div id="app"><router-view></router-view></div>';
+		document.body.innerHTML = '<div id="app"></div>';
 
 		const app = new Vue({
-			router: new VueRouter()
+			router: new VueRouter({
+				mode: 'history'
+			}),
+			render: createEl => createEl(App)
 		}).$mount('#app');
 
 		try {

From 08ee9e4eaffa4b6809440ec2cbe4daad084c00df Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 14:56:33 +0900
Subject: [PATCH 027/286] wip

---
 src/web/app/common/mios.ts                    |   4 +-
 src/web/app/desktop/router.ts                 |   2 +-
 src/web/app/desktop/script.ts                 |  15 ++-
 src/web/app/desktop/style.styl                |  10 +-
 src/web/app/desktop/tags/pages/index.vue      |   3 -
 src/web/app/desktop/views/components/index.ts |   5 +
 src/web/app/desktop/views/components/ui.vue   |   6 +
 src/web/app/desktop/{tags => views}/home.vue  |   2 -
 src/web/app/desktop/views/pages/home.vue      |  17 +++
 src/web/app/desktop/views/pages/index.vue     |  17 +++
 src/web/app/desktop/views/pages/welcome.vue   | 119 ++++++++++++++++++
 src/web/app/init.ts                           |  18 +--
 src/web/app/mobile/router.ts                  |   2 +-
 webpack/module/rules/theme-color.ts           |   2 +-
 14 files changed, 193 insertions(+), 29 deletions(-)
 delete mode 100644 src/web/app/desktop/tags/pages/index.vue
 create mode 100644 src/web/app/desktop/views/components/index.ts
 create mode 100644 src/web/app/desktop/views/components/ui.vue
 rename src/web/app/desktop/{tags => views}/home.vue (99%)
 create mode 100644 src/web/app/desktop/views/pages/home.vue
 create mode 100644 src/web/app/desktop/views/pages/index.vue
 create mode 100644 src/web/app/desktop/views/pages/welcome.vue

diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index 4ff2333e8..e91def521 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -38,7 +38,7 @@ export default class MiOS extends EventEmitter {
 	/**
 	 * Whether signed in
 	 */
-	public get isSignedin() {
+	public get isSignedIn() {
 		return this.i != null;
 	}
 
@@ -251,7 +251,7 @@ export default class MiOS extends EventEmitter {
 		if (!isSwSupported) return;
 
 		// Reject when not signed in to Misskey
-		if (!this.isSignedin) return;
+		if (!this.isSignedIn) return;
 
 		// When service worker activated
 		navigator.serviceWorker.ready.then(registration => {
diff --git a/src/web/app/desktop/router.ts b/src/web/app/desktop/router.ts
index ce68c4f2d..6ba8bda12 100644
--- a/src/web/app/desktop/router.ts
+++ b/src/web/app/desktop/router.ts
@@ -23,7 +23,7 @@ export default (mios: MiOS) => {
 	route('*',                       notFound);
 
 	function index() {
-		mios.isSignedin ? home() : entrance();
+		mios.isSignedIn ? home() : entrance();
 	}
 
 	function home() {
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index 4aef69b07..e4e5f1914 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -5,19 +5,17 @@
 // Style
 import './style.styl';
 
-import Vue from 'vue';
 import init from '../init';
 import fuckAdBlock from './scripts/fuck-ad-block';
-import MiOS from '../common/mios';
 import HomeStreamManager from '../common/scripts/streaming/home-stream-manager';
 import composeNotification from '../common/scripts/compose-notification';
 
-import MkIndex from './tags/pages/index.vue';
+import MkIndex from './views/pages/index.vue';
 
 /**
  * init
  */
-init(async (mios: MiOS, app: Vue) => {
+init(async (os, launch) => {
 	/**
 	 * Fuck AD Block
 	 */
@@ -33,12 +31,17 @@ init(async (mios: MiOS, app: Vue) => {
 		}
 
 		if ((Notification as any).permission == 'granted') {
-			registerNotifications(mios.stream);
+			registerNotifications(os.stream);
 		}
 	}
 
+	// Register components
+	require('./views/components');
+
+	const app = launch();
+
 	app.$router.addRoutes([{
-		path: '/', component: MkIndex, props: { os: mios }
+		path: '/', component: MkIndex, props: { os }
 	}]);
 }, true);
 
diff --git a/src/web/app/desktop/style.styl b/src/web/app/desktop/style.styl
index c893e2ed6..4d295035f 100644
--- a/src/web/app/desktop/style.styl
+++ b/src/web/app/desktop/style.styl
@@ -42,10 +42,10 @@
 		background rgba(0, 0, 0, 0.2)
 
 html
+	height 100%
 	background #f7f7f7
 
-	// ↓ workaround of https://github.com/riot/riot/issues/2134
-	&[data-page='entrance']
-		#wait
-			right auto
-			left 15px
+body
+	display flex
+	flex-direction column
+	min-height 100%
diff --git a/src/web/app/desktop/tags/pages/index.vue b/src/web/app/desktop/tags/pages/index.vue
deleted file mode 100644
index 6bd036fc2..000000000
--- a/src/web/app/desktop/tags/pages/index.vue
+++ /dev/null
@@ -1,3 +0,0 @@
-<template>
-	<h1>hi</h1>
-</template>
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
new file mode 100644
index 000000000..f628dee88
--- /dev/null
+++ b/src/web/app/desktop/views/components/index.ts
@@ -0,0 +1,5 @@
+import Vue from 'vue';
+
+import ui from './ui.vue';
+
+Vue.component('mk-ui', ui);
diff --git a/src/web/app/desktop/views/components/ui.vue b/src/web/app/desktop/views/components/ui.vue
new file mode 100644
index 000000000..34ac86f70
--- /dev/null
+++ b/src/web/app/desktop/views/components/ui.vue
@@ -0,0 +1,6 @@
+<template>
+<div>
+	<header>misskey</header>
+	<slot></slot>
+</div>
+</template>
diff --git a/src/web/app/desktop/tags/home.vue b/src/web/app/desktop/views/home.vue
similarity index 99%
rename from src/web/app/desktop/tags/home.vue
rename to src/web/app/desktop/views/home.vue
index 981123c56..d054127da 100644
--- a/src/web/app/desktop/tags/home.vue
+++ b/src/web/app/desktop/views/home.vue
@@ -82,8 +82,6 @@
 <script lang="typescript">
 import uuid from 'uuid';
 import Sortable from 'sortablejs';
-import I from '../../common/i';
-import { resolveSrv } from 'dns';
 
 export default {
 	props: {
diff --git a/src/web/app/desktop/views/pages/home.vue b/src/web/app/desktop/views/pages/home.vue
new file mode 100644
index 000000000..8a380fad0
--- /dev/null
+++ b/src/web/app/desktop/views/pages/home.vue
@@ -0,0 +1,17 @@
+<template>
+	<mk-ui>
+		<home ref="home" :mode="mode"/>
+	</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		mode: {
+			type: String,
+			default: 'timeline'
+		}
+	},
+});
+</script>
diff --git a/src/web/app/desktop/views/pages/index.vue b/src/web/app/desktop/views/pages/index.vue
new file mode 100644
index 000000000..dbe77e081
--- /dev/null
+++ b/src/web/app/desktop/views/pages/index.vue
@@ -0,0 +1,17 @@
+<template>
+	<component v-bind:is="os.isSignedIn ? 'home' : 'welcome'"></component>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import HomeView from './home.vue';
+import WelcomeView from './welcome.vue';
+
+export default Vue.extend({
+	props: ['os'],
+	components: {
+		home: HomeView,
+		welcome: WelcomeView
+	}
+});
+</script>
diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
new file mode 100644
index 000000000..a99a31d6b
--- /dev/null
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -0,0 +1,119 @@
+<template>
+<div class="root">
+	<main>
+		<div>
+			<h1>繋がるNotes</h1>
+			<p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです――思ったこと、共有したいことをシンプルに書き残せます。タイムラインを見れば、皆の反応や皆がどう思っているのかもすぐにわかります。<a>詳しく...</a></p>
+			<p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
+		</div>
+		<div>
+
+		</div>
+	</main>
+	<mk-forkit/>
+	<footer>
+		<div>
+			<mk-nav-links/>
+			<p class="c">{ _COPYRIGHT_ }</p>
+		</div>
+	</footer>
+</div>
+</template>
+
+<style>
+	#wait {
+		right: auto;
+		left: 15px;
+	}
+</style>
+
+<style lang="stylus" scoped>
+	.root
+		display flex
+		flex-direction column
+		flex 1
+		background #eee
+		$width = 1000px
+
+		> main
+			display flex
+			flex 1
+			max-width $width
+			margin 0 auto
+			padding 80px 0 0 0
+
+			> div:first-child
+				margin 0 auto 0 0
+				width calc(100% - 500px)
+				color #777
+
+				> h1
+					margin 0
+					font-weight normal
+
+				> p
+					margin 0.5em 0
+					line-height 2em
+
+				button
+					padding 8px 16px
+					font-size inherit
+
+				.signup
+					color $theme-color
+					border solid 2px $theme-color
+					border-radius 4px
+
+					&:focus
+						box-shadow 0 0 0 3px rgba($theme-color, 0.2)
+
+					&:hover
+						color $theme-color-foreground
+						background $theme-color
+
+					&:active
+						color $theme-color-foreground
+						background darken($theme-color, 10%)
+						border-color darken($theme-color, 10%)
+
+				.signin
+					&:focus
+						color #444
+
+					&:hover
+						color #444
+
+					&:active
+						color #333
+
+			> div:last-child
+				margin 0 0 0 auto
+
+		> footer
+			background #fff
+
+			*
+				color #fff !important
+				text-shadow 0 0 8px #000
+				font-weight bold
+
+			> div
+				max-width $width
+				margin 0 auto
+				padding 16px 0
+				text-align center
+				border-top solid 1px #fff
+
+				> .c
+					margin 0
+					line-height 64px
+					font-size 10px
+
+</style>
+
+<script lang="ts">
+import Vue from 'vue'
+export default Vue.extend({
+
+})
+</script>
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 91797a95a..796a96694 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -61,22 +61,24 @@ if (localStorage.getItem('should-refresh') == 'true') {
 }
 
 // MiOSを初期化してコールバックする
-export default (callback, sw = false) => {
+export default (callback: (os: MiOS, launch: () => Vue) => void, sw = false) => {
 	const mios = new MiOS(sw);
 
 	mios.init(() => {
 		// アプリ基底要素マウント
 		document.body.innerHTML = '<div id="app"></div>';
 
-		const app = new Vue({
-			router: new VueRouter({
-				mode: 'history'
-			}),
-			render: createEl => createEl(App)
-		}).$mount('#app');
+		const launch = () => {
+			return new Vue({
+				router: new VueRouter({
+					mode: 'history'
+				}),
+				render: createEl => createEl(App)
+			}).$mount('#app');
+		};
 
 		try {
-			callback(mios, app);
+			callback(mios, launch);
 		} catch (e) {
 			panic(e);
 		}
diff --git a/src/web/app/mobile/router.ts b/src/web/app/mobile/router.ts
index afb9aa620..050fa7fc2 100644
--- a/src/web/app/mobile/router.ts
+++ b/src/web/app/mobile/router.ts
@@ -32,7 +32,7 @@ export default (mios: MiOS) => {
 	route('*',                           notFound);
 
 	function index() {
-		mios.isSignedin ? home() : entrance();
+		mios.isSignedIn ? home() : entrance();
 	}
 
 	function home() {
diff --git a/webpack/module/rules/theme-color.ts b/webpack/module/rules/theme-color.ts
index 7ee545191..a65338465 100644
--- a/webpack/module/rules/theme-color.ts
+++ b/webpack/module/rules/theme-color.ts
@@ -8,7 +8,7 @@ const constants = require('../../../src/const.json');
 
 export default () => ({
 	enforce: 'pre',
-	test: /\.tag$/,
+	test: /\.vue$/,
 	exclude: /node_modules/,
 	loader: StringReplacePlugin.replace({
 		replacements: [

From dd60907abec23520c96b736bf3d91160e63a67fe Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 15:06:11 +0900
Subject: [PATCH 028/286] wip

---
 src/web/app/desktop/views/pages/welcome.vue | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index a99a31d6b..c0e1c0bd4 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -2,7 +2,7 @@
 <div class="root">
 	<main>
 		<div>
-			<h1>繋がるNotes</h1>
+			<h1>Share<br>Everything!</h1>
 			<p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです――思ったこと、共有したいことをシンプルに書き残せます。タイムラインを見れば、皆の反応や皆がどう思っているのかもすぐにわかります。<a>詳しく...</a></p>
 			<p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
 		</div>
@@ -50,6 +50,8 @@
 				> h1
 					margin 0
 					font-weight normal
+					font-variant small-caps
+					letter-spacing 12px
 
 				> p
 					margin 0.5em 0

From 4f1795b97b43e324d47653c5b172afa984446868 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 16:22:14 +0900
Subject: [PATCH 029/286] wip

---
 package.json                                  |   1 +
 src/web/app/common/-tags/signin.tag           | 155 --------
 src/web/app/common/-tags/signup.tag           | 307 ----------------
 src/web/app/common/views/components/index.ts  |   7 +
 .../{tags => views/components}/poll.vue       |   0
 .../components}/reaction-icon.vue             |   0
 .../components}/reaction-picker.vue           |   0
 .../components}/reactions-viewer.vue          |   0
 .../app/common/views/components/signin.vue    | 138 ++++++++
 .../app/common/views/components/signup.vue    | 331 ++++++++++++++++++
 .../components}/stream-indicator.vue          |   0
 .../{tags => views/components}/time.vue       |   0
 .../components}/url-preview.vue               |   0
 .../common/{tags => views/components}/url.vue |   0
 src/web/app/desktop/views/pages/welcome.vue   | 186 +++++-----
 src/web/app/init.ts                           |   2 +
 16 files changed, 576 insertions(+), 551 deletions(-)
 delete mode 100644 src/web/app/common/-tags/signin.tag
 delete mode 100644 src/web/app/common/-tags/signup.tag
 create mode 100644 src/web/app/common/views/components/index.ts
 rename src/web/app/common/{tags => views/components}/poll.vue (100%)
 rename src/web/app/common/{tags => views/components}/reaction-icon.vue (100%)
 rename src/web/app/common/{tags => views/components}/reaction-picker.vue (100%)
 rename src/web/app/common/{tags => views/components}/reactions-viewer.vue (100%)
 create mode 100644 src/web/app/common/views/components/signin.vue
 create mode 100644 src/web/app/common/views/components/signup.vue
 rename src/web/app/common/{tags => views/components}/stream-indicator.vue (100%)
 rename src/web/app/common/{tags => views/components}/time.vue (100%)
 rename src/web/app/common/{tags => views/components}/url-preview.vue (100%)
 rename src/web/app/common/{tags => views/components}/url.vue (100%)

diff --git a/package.json b/package.json
index 56501266b..fee512c7f 100644
--- a/package.json
+++ b/package.json
@@ -173,6 +173,7 @@
 		"uuid": "3.2.1",
 		"vhost": "3.0.2",
 		"vue": "^2.5.13",
+		"vue-js-modal": "^1.3.9",
 		"vue-loader": "^14.1.1",
 		"vue-router": "^3.0.1",
 		"vue-template-compiler": "^2.5.13",
diff --git a/src/web/app/common/-tags/signin.tag b/src/web/app/common/-tags/signin.tag
deleted file mode 100644
index 89213d1f7..000000000
--- a/src/web/app/common/-tags/signin.tag
+++ /dev/null
@@ -1,155 +0,0 @@
-<mk-signin>
-	<form :class="{ signing: signing }" onsubmit={ onsubmit }>
-		<label class="user-name">
-			<input ref="username" type="text" pattern="^[a-zA-Z0-9-]+$" placeholder="%i18n:common.tags.mk-signin.username%" autofocus="autofocus" required="required" oninput={ oninput }/>%fa:at%
-		</label>
-		<label class="password">
-			<input ref="password" type="password" placeholder="%i18n:common.tags.mk-signin.password%" required="required"/>%fa:lock%
-		</label>
-		<label class="token" v-if="user && user.two_factor_enabled">
-			<input ref="token" type="number" placeholder="%i18n:common.tags.mk-signin.token%" required="required"/>%fa:lock%
-		</label>
-		<button type="submit" disabled={ signing }>{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }</button>
-	</form>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> form
-				display block
-				z-index 2
-
-				&.signing
-					&, *
-						cursor wait !important
-
-				label
-					display block
-					margin 12px 0
-
-					[data-fa]
-						display block
-						pointer-events none
-						position absolute
-						bottom 0
-						top 0
-						left 0
-						z-index 1
-						margin auto
-						padding 0 16px
-						height 1em
-						color #898786
-
-					input[type=text]
-					input[type=password]
-					input[type=number]
-						user-select text
-						display inline-block
-						cursor auto
-						padding 0 0 0 38px
-						margin 0
-						width 100%
-						line-height 44px
-						font-size 1em
-						color rgba(0, 0, 0, 0.7)
-						background #fff
-						outline none
-						border solid 1px #eee
-						border-radius 4px
-
-						&:hover
-							background rgba(255, 255, 255, 0.7)
-							border-color #ddd
-
-							& + i
-								color #797776
-
-						&:focus
-							background #fff
-							border-color #ccc
-
-							& + i
-								color #797776
-
-				[type=submit]
-					cursor pointer
-					padding 16px
-					margin -6px 0 0 0
-					width 100%
-					font-size 1.2em
-					color rgba(0, 0, 0, 0.5)
-					outline none
-					border none
-					border-radius 0
-					background transparent
-					transition all .5s ease
-
-					&:hover
-						color $theme-color
-						transition all .2s ease
-
-					&:focus
-						color $theme-color
-						transition all .2s ease
-
-					&:active
-						color darken($theme-color, 30%)
-						transition all .2s ease
-
-					&:disabled
-						opacity 0.7
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.user = null;
-		this.signing = false;
-
-		this.oninput = () => {
-			this.api('users/show', {
-				username: this.$refs.username.value
-			}).then(user => {
-				this.user = user;
-				this.$emit('user', user);
-				this.update();
-			});
-		};
-
-		this.onsubmit = e => {
-			e.preventDefault();
-
-			if (this.$refs.username.value == '') {
-				this.$refs.username.focus();
-				return false;
-			}
-			if (this.$refs.password.value == '') {
-				this.$refs.password.focus();
-				return false;
-			}
-			if (this.user && this.user.two_factor_enabled && this.$refs.token.value == '') {
-				this.$refs.token.focus();
-				return false;
-			}
-
-			this.update({
-				signing: true
-			});
-
-			this.api('signin', {
-				username: this.$refs.username.value,
-				password: this.$refs.password.value,
-				token: this.user && this.user.two_factor_enabled ? this.$refs.token.value : undefined
-			}).then(() => {
-				location.reload();
-			}).catch(() => {
-				alert('something happened');
-				this.update({
-					signing: false
-				});
-			});
-
-			return false;
-		};
-	</script>
-</mk-signin>
diff --git a/src/web/app/common/-tags/signup.tag b/src/web/app/common/-tags/signup.tag
deleted file mode 100644
index 99be10609..000000000
--- a/src/web/app/common/-tags/signup.tag
+++ /dev/null
@@ -1,307 +0,0 @@
-<mk-signup>
-	<form onsubmit={ onsubmit } autocomplete="off">
-		<label class="username">
-			<p class="caption">%fa:at%%i18n:common.tags.mk-signup.username%</p>
-			<input ref="username" type="text" pattern="^[a-zA-Z0-9-]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required="required" onkeyup={ onChangeUsername }/>
-			<p class="profile-page-url-preview" v-if="refs.username.value != '' && username-state != 'invalidFormat' && username-state != 'minRange' && username-state != 'maxRange'">{ _URL_ + '/' + refs.username.value }</p>
-			<p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:common.tags.mk-signup.checking%</p>
-			<p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.available%</p>
-			<p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.unavailable%</p>
-			<p class="info" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.error%</p>
-			<p class="info" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.invalid-format%</p>
-			<p class="info" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-short%</p>
-			<p class="info" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-long%</p>
-		</label>
-		<label class="password">
-			<p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%</p>
-			<input ref="password" type="password" placeholder="%i18n:common.tags.mk-signup.password-placeholder%" autocomplete="off" required="required" onkeyup={ onChangePassword }/>
-			<div class="meter" v-if="passwordStrength != ''" data-strength={ passwordStrength }>
-				<div class="value" ref="passwordMetar"></div>
-			</div>
-			<p class="info" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.weak-password%</p>
-			<p class="info" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.normal-password%</p>
-			<p class="info" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.strong-password%</p>
-		</label>
-		<label class="retype-password">
-			<p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%(%i18n:common.tags.mk-signup.retype%)</p>
-			<input ref="passwordRetype" type="password" placeholder="%i18n:common.tags.mk-signup.retype-placeholder%" autocomplete="off" required="required" onkeyup={ onChangePasswordRetype }/>
-			<p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.password-matched%</p>
-			<p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.password-not-matched%</p>
-		</label>
-		<label class="recaptcha">
-			<p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:common.tags.mk-signup.recaptcha%</p>
-			<div v-if="recaptcha" class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" data-sitekey={ recaptcha.site_key }></div>
-		</label>
-		<label class="agree-tou">
-			<input name="agree-tou" type="checkbox" autocomplete="off" required="required"/>
-			<p><a href={ touUrl } target="_blank">利用規約</a>に同意する</p>
-		</label>
-		<button @click="onsubmit">%i18n:common.tags.mk-signup.create%</button>
-	</form>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			min-width 302px
-			overflow hidden
-
-			> form
-
-				label
-					display block
-					margin 16px 0
-
-					> .caption
-						margin 0 0 4px 0
-						color #828888
-						font-size 0.95em
-
-						> [data-fa]
-							margin-right 0.25em
-							color #96adac
-
-					> .info
-						display block
-						margin 4px 0
-						font-size 0.8em
-
-						> [data-fa]
-							margin-right 0.3em
-
-					&.username
-						.profile-page-url-preview
-							display block
-							margin 4px 8px 0 4px
-							font-size 0.8em
-							color #888
-
-							&:empty
-								display none
-
-							&:not(:empty) + .info
-								margin-top 0
-
-					&.password
-						.meter
-							display block
-							margin-top 8px
-							width 100%
-							height 8px
-
-							&[data-strength='']
-								display none
-
-							&[data-strength='low']
-								> .value
-									background #d73612
-
-							&[data-strength='medium']
-								> .value
-									background #d7ca12
-
-							&[data-strength='high']
-								> .value
-									background #61bb22
-
-							> .value
-								display block
-								width 0%
-								height 100%
-								background transparent
-								border-radius 4px
-								transition all 0.1s ease
-
-				[type=text], [type=password]
-					user-select text
-					display inline-block
-					cursor auto
-					padding 0 12px
-					margin 0
-					width 100%
-					line-height 44px
-					font-size 1em
-					color #333 !important
-					background #fff !important
-					outline none
-					border solid 1px rgba(0, 0, 0, 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)
-						transition all .1s ease
-
-					&:focus
-						color $theme-color !important
-						border-color $theme-color
-						box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%)
-						transition all 0s ease
-
-					&:disabled
-						opacity 0.5
-
-				.agree-tou
-					padding 4px
-					border-radius 4px
-
-					&:hover
-						background #f4f4f4
-
-					&:active
-						background #eee
-
-					&, *
-						cursor pointer
-
-					p
-						display inline
-						color #555
-
-				button
-					margin 0 0 32px 0
-					padding 16px
-					width 100%
-					font-size 1em
-					color #fff
-					background $theme-color
-					border-radius 3px
-
-					&:hover
-						background lighten($theme-color, 5%)
-
-					&:active
-						background darken($theme-color, 5%)
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-		const getPasswordStrength = require('syuilo-password-strength');
-
-		this.usernameState = null;
-		this.passwordStrength = '';
-		this.passwordRetypeState = null;
-		this.recaptchaed = false;
-
-		this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/tou`;
-
-		window.onRecaptchaed = () => {
-			this.recaptchaed = true;
-			this.update();
-		};
-
-		window.onRecaptchaExpired = () => {
-			this.recaptchaed = false;
-			this.update();
-		};
-
-		this.on('mount', () => {
-			this.update({
-				recaptcha: {
-					site_key: _RECAPTCHA_SITEKEY_
-				}
-			});
-
-			const head = document.getElementsByTagName('head')[0];
-			const script = document.createElement('script');
-			script.setAttribute('src', 'https://www.google.com/recaptcha/api.js');
-			head.appendChild(script);
-		});
-
-		this.onChangeUsername = () => {
-			const username = this.$refs.username.value;
-
-			if (username == '') {
-				this.update({
-					usernameState: null
-				});
-				return;
-			}
-
-			const err =
-				!username.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
-				username.length < 3 ? 'min-range' :
-				username.length > 20 ? 'max-range' :
-				null;
-
-			if (err) {
-				this.update({
-					usernameState: err
-				});
-				return;
-			}
-
-			this.update({
-				usernameState: 'wait'
-			});
-
-			this.api('username/available', {
-				username: username
-			}).then(result => {
-				this.update({
-					usernameState: result.available ? 'ok' : 'unavailable'
-				});
-			}).catch(err => {
-				this.update({
-					usernameState: 'error'
-				});
-			});
-		};
-
-		this.onChangePassword = () => {
-			const password = this.$refs.password.value;
-
-			if (password == '') {
-				this.passwordStrength = '';
-				return;
-			}
-
-			const strength = getPasswordStrength(password);
-			this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
-			this.update();
-			this.$refs.passwordMetar.style.width = `${strength * 100}%`;
-		};
-
-		this.onChangePasswordRetype = () => {
-			const password = this.$refs.password.value;
-			const retypedPassword = this.$refs.passwordRetype.value;
-
-			if (retypedPassword == '') {
-				this.passwordRetypeState = null;
-				return;
-			}
-
-			this.passwordRetypeState = password == retypedPassword ? 'match' : 'not-match';
-		};
-
-		this.onsubmit = e => {
-			e.preventDefault();
-
-			const username = this.$refs.username.value;
-			const password = this.$refs.password.value;
-
-			const locker = document.body.appendChild(document.createElement('mk-locker'));
-
-			this.api('signup', {
-				username: username,
-				password: password,
-				'g-recaptcha-response': grecaptcha.getResponse()
-			}).then(() => {
-				this.api('signin', {
-					username: username,
-					password: password
-				}).then(() => {
-					location.href = '/';
-				});
-			}).catch(() => {
-				alert('%i18n:common.tags.mk-signup.some-error%');
-
-				grecaptcha.reset();
-				this.recaptchaed = false;
-
-				locker.parentNode.removeChild(locker);
-			});
-
-			return false;
-		};
-	</script>
-</mk-signup>
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
new file mode 100644
index 000000000..b1c5df819
--- /dev/null
+++ b/src/web/app/common/views/components/index.ts
@@ -0,0 +1,7 @@
+import Vue from 'vue';
+
+import signin from './signin.vue';
+import signup from './signup.vue';
+
+Vue.component('mk-signin', signin);
+Vue.component('mk-signup', signup);
diff --git a/src/web/app/common/tags/poll.vue b/src/web/app/common/views/components/poll.vue
similarity index 100%
rename from src/web/app/common/tags/poll.vue
rename to src/web/app/common/views/components/poll.vue
diff --git a/src/web/app/common/tags/reaction-icon.vue b/src/web/app/common/views/components/reaction-icon.vue
similarity index 100%
rename from src/web/app/common/tags/reaction-icon.vue
rename to src/web/app/common/views/components/reaction-icon.vue
diff --git a/src/web/app/common/tags/reaction-picker.vue b/src/web/app/common/views/components/reaction-picker.vue
similarity index 100%
rename from src/web/app/common/tags/reaction-picker.vue
rename to src/web/app/common/views/components/reaction-picker.vue
diff --git a/src/web/app/common/tags/reactions-viewer.vue b/src/web/app/common/views/components/reactions-viewer.vue
similarity index 100%
rename from src/web/app/common/tags/reactions-viewer.vue
rename to src/web/app/common/views/components/reactions-viewer.vue
diff --git a/src/web/app/common/views/components/signin.vue b/src/web/app/common/views/components/signin.vue
new file mode 100644
index 000000000..5ffc518b3
--- /dev/null
+++ b/src/web/app/common/views/components/signin.vue
@@ -0,0 +1,138 @@
+<template>
+<form class="form" :class="{ signing: signing }" @submit.prevent="onSubmit">
+	<label class="user-name">
+		<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]+$" placeholder="%i18n:common.tags.mk-signin.username%" autofocus required @change="onUsernameChange"/>%fa:at%
+	</label>
+	<label class="password">
+		<input v-model="password" type="password" placeholder="%i18n:common.tags.mk-signin.password%" required/>%fa:lock%
+	</label>
+	<label class="token" v-if="user && user.two_factor_enabled">
+		<input v-model="token" type="number" placeholder="%i18n:common.tags.mk-signin.token%" required/>%fa:lock%
+	</label>
+	<button type="submit" disabled={ signing }>{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }</button>
+</form>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['os'],
+	data() {
+		return {
+			signing: false,
+			user: null
+		};
+	},
+	methods: {
+		onUsernameChange() {
+			this.os.api('users/show', {
+				username: this.username
+			}).then(user => {
+				this.user = user;
+			});
+		},
+		onSubmit() {
+			this.signing = true;
+
+			this.os.api('signin', {
+				username: this.username,
+				password: this.password,
+				token: this.user && this.user.two_factor_enabled ? this.token : undefined
+			}).then(() => {
+				location.reload();
+			}).catch(() => {
+				alert('something happened');
+				this.signing = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.form
+	display block
+	z-index 2
+
+	&.signing
+		&, *
+			cursor wait !important
+
+	label
+		display block
+		margin 12px 0
+
+		[data-fa]
+			display block
+			pointer-events none
+			position absolute
+			bottom 0
+			top 0
+			left 0
+			z-index 1
+			margin auto
+			padding 0 16px
+			height 1em
+			color #898786
+
+		input[type=text]
+		input[type=password]
+		input[type=number]
+			user-select text
+			display inline-block
+			cursor auto
+			padding 0 0 0 38px
+			margin 0
+			width 100%
+			line-height 44px
+			font-size 1em
+			color rgba(0, 0, 0, 0.7)
+			background #fff
+			outline none
+			border solid 1px #eee
+			border-radius 4px
+
+			&:hover
+				background rgba(255, 255, 255, 0.7)
+				border-color #ddd
+
+				& + i
+					color #797776
+
+			&:focus
+				background #fff
+				border-color #ccc
+
+				& + i
+					color #797776
+
+	[type=submit]
+		cursor pointer
+		padding 16px
+		margin -6px 0 0 0
+		width 100%
+		font-size 1.2em
+		color rgba(0, 0, 0, 0.5)
+		outline none
+		border none
+		border-radius 0
+		background transparent
+		transition all .5s ease
+
+		&:hover
+			color $theme-color
+			transition all .2s ease
+
+		&:focus
+			color $theme-color
+			transition all .2s ease
+
+		&:active
+			color darken($theme-color, 30%)
+			transition all .2s ease
+
+		&:disabled
+			opacity 0.7
+
+</style>
diff --git a/src/web/app/common/views/components/signup.vue b/src/web/app/common/views/components/signup.vue
new file mode 100644
index 000000000..1734f7731
--- /dev/null
+++ b/src/web/app/common/views/components/signup.vue
@@ -0,0 +1,331 @@
+<template>
+<form @submit.prevent="onSubmit" autocomplete="off">
+	<label class="username">
+		<p class="caption">%fa:at%%i18n:common.tags.mk-signup.username%</p>
+		<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @keyup="onChangeUsername"/>
+		<p class="profile-page-url-preview" v-if="refs.username.value != '' && username-state != 'invalidFormat' && username-state != 'minRange' && username-state != 'maxRange'">{ _URL_ + '/' + refs.username.value }</p>
+		<p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:common.tags.mk-signup.checking%</p>
+		<p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.available%</p>
+		<p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.unavailable%</p>
+		<p class="info" v-if="usernameState == 'error'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.error%</p>
+		<p class="info" v-if="usernameState == 'invalid-format'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.invalid-format%</p>
+		<p class="info" v-if="usernameState == 'min-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-short%</p>
+		<p class="info" v-if="usernameState == 'max-range'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.too-long%</p>
+	</label>
+	<label class="password">
+		<p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%</p>
+		<input v-model="password" type="password" placeholder="%i18n:common.tags.mk-signup.password-placeholder%" autocomplete="off" required @keyup="onChangePassword"/>
+		<div class="meter" v-if="passwordStrength != ''" :data-strength="passwordStrength">
+			<div class="value" ref="passwordMetar"></div>
+		</div>
+		<p class="info" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.weak-password%</p>
+		<p class="info" v-if="passwordStrength == 'medium'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.normal-password%</p>
+		<p class="info" v-if="passwordStrength == 'high'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.strong-password%</p>
+	</label>
+	<label class="retype-password">
+		<p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%(%i18n:common.tags.mk-signup.retype%)</p>
+		<input v-model="passwordRetype" type="password" placeholder="%i18n:common.tags.mk-signup.retype-placeholder%" autocomplete="off" required @keyup="onChangePasswordRetype"/>
+		<p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.password-matched%</p>
+		<p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.password-not-matched%</p>
+	</label>
+	<label class="recaptcha">
+		<p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:common.tags.mk-signup.recaptcha%</p>
+		<div v-if="recaptcha" class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" data-sitekey="recaptcha.site_key"></div>
+	</label>
+	<label class="agree-tou">
+		<input name="agree-tou" type="checkbox" autocomplete="off" required/>
+		<p><a :href="touUrl" target="_blank">利用規約</a>に同意する</p>
+	</label>
+	<button type="submit">%i18n:common.tags.mk-signup.create%</button>
+</form>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+const getPasswordStrength = require('syuilo-password-strength');
+import
+
+const aboutUrl = `${_DOCS_URL_}/${_LANG_}/tou`;
+
+export default Vue.extend({
+	methods: {
+		onSubmit() {
+
+		}
+	},
+	mounted() {
+		const head = document.getElementsByTagName('head')[0];
+		const script = document.createElement('script');
+		script.setAttribute('src', 'https://www.google.com/recaptcha/api.js');
+		head.appendChild(script);
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+	:scope
+		display block
+		min-width 302px
+		overflow hidden
+
+		> form
+
+			label
+				display block
+				margin 16px 0
+
+				> .caption
+					margin 0 0 4px 0
+					color #828888
+					font-size 0.95em
+
+					> [data-fa]
+						margin-right 0.25em
+						color #96adac
+
+				> .info
+					display block
+					margin 4px 0
+					font-size 0.8em
+
+					> [data-fa]
+						margin-right 0.3em
+
+				&.username
+					.profile-page-url-preview
+						display block
+						margin 4px 8px 0 4px
+						font-size 0.8em
+						color #888
+
+						&:empty
+							display none
+
+						&:not(:empty) + .info
+							margin-top 0
+
+				&.password
+					.meter
+						display block
+						margin-top 8px
+						width 100%
+						height 8px
+
+						&[data-strength='']
+							display none
+
+						&[data-strength='low']
+							> .value
+								background #d73612
+
+						&[data-strength='medium']
+							> .value
+								background #d7ca12
+
+						&[data-strength='high']
+							> .value
+								background #61bb22
+
+						> .value
+							display block
+							width 0%
+							height 100%
+							background transparent
+							border-radius 4px
+							transition all 0.1s ease
+
+			[type=text], [type=password]
+				user-select text
+				display inline-block
+				cursor auto
+				padding 0 12px
+				margin 0
+				width 100%
+				line-height 44px
+				font-size 1em
+				color #333 !important
+				background #fff !important
+				outline none
+				border solid 1px rgba(0, 0, 0, 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)
+					transition all .1s ease
+
+				&:focus
+					color $theme-color !important
+					border-color $theme-color
+					box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%)
+					transition all 0s ease
+
+				&:disabled
+					opacity 0.5
+
+			.agree-tou
+				padding 4px
+				border-radius 4px
+
+				&:hover
+					background #f4f4f4
+
+				&:active
+					background #eee
+
+				&, *
+					cursor pointer
+
+				p
+					display inline
+					color #555
+
+			button
+				margin 0 0 32px 0
+				padding 16px
+				width 100%
+				font-size 1em
+				color #fff
+				background $theme-color
+				border-radius 3px
+
+				&:hover
+					background lighten($theme-color, 5%)
+
+				&:active
+					background darken($theme-color, 5%)
+
+</style>
+
+<script lang="typescript">
+	this.mixin('api');
+
+
+	this.usernameState = null;
+	this.passwordStrength = '';
+	this.passwordRetypeState = null;
+	this.recaptchaed = false;
+
+	this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/tou`;
+
+	window.onRecaptchaed = () => {
+		this.recaptchaed = true;
+		this.update();
+	};
+
+	window.onRecaptchaExpired = () => {
+		this.recaptchaed = false;
+		this.update();
+	};
+
+	this.on('mount', () => {
+		this.update({
+			recaptcha: {
+				site_key: _RECAPTCHA_SITEKEY_
+			}
+		});
+
+		const head = document.getElementsByTagName('head')[0];
+		const script = document.createElement('script');
+		script.setAttribute('src', 'https://www.google.com/recaptcha/api.js');
+		head.appendChild(script);
+	});
+
+	this.onChangeUsername = () => {
+		const username = this.$refs.username.value;
+
+		if (username == '') {
+			this.update({
+				usernameState: null
+			});
+			return;
+		}
+
+		const err =
+			!username.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
+			username.length < 3 ? 'min-range' :
+			username.length > 20 ? 'max-range' :
+			null;
+
+		if (err) {
+			this.update({
+				usernameState: err
+			});
+			return;
+		}
+
+		this.update({
+			usernameState: 'wait'
+		});
+
+		this.api('username/available', {
+			username: username
+		}).then(result => {
+			this.update({
+				usernameState: result.available ? 'ok' : 'unavailable'
+			});
+		}).catch(err => {
+			this.update({
+				usernameState: 'error'
+			});
+		});
+	};
+
+	this.onChangePassword = () => {
+		const password = this.$refs.password.value;
+
+		if (password == '') {
+			this.passwordStrength = '';
+			return;
+		}
+
+		const strength = getPasswordStrength(password);
+		this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
+		this.update();
+		this.$refs.passwordMetar.style.width = `${strength * 100}%`;
+	};
+
+	this.onChangePasswordRetype = () => {
+		const password = this.$refs.password.value;
+		const retypedPassword = this.$refs.passwordRetype.value;
+
+		if (retypedPassword == '') {
+			this.passwordRetypeState = null;
+			return;
+		}
+
+		this.passwordRetypeState = password == retypedPassword ? 'match' : 'not-match';
+	};
+
+	this.onsubmit = e => {
+		e.preventDefault();
+
+		const username = this.$refs.username.value;
+		const password = this.$refs.password.value;
+
+		const locker = document.body.appendChild(document.createElement('mk-locker'));
+
+		this.api('signup', {
+			username: username,
+			password: password,
+			'g-recaptcha-response': grecaptcha.getResponse()
+		}).then(() => {
+			this.api('signin', {
+				username: username,
+				password: password
+			}).then(() => {
+				location.href = '/';
+			});
+		}).catch(() => {
+			alert('%i18n:common.tags.mk-signup.some-error%');
+
+			grecaptcha.reset();
+			this.recaptchaed = false;
+
+			locker.parentNode.removeChild(locker);
+		});
+
+		return false;
+	};
+</script>
diff --git a/src/web/app/common/tags/stream-indicator.vue b/src/web/app/common/views/components/stream-indicator.vue
similarity index 100%
rename from src/web/app/common/tags/stream-indicator.vue
rename to src/web/app/common/views/components/stream-indicator.vue
diff --git a/src/web/app/common/tags/time.vue b/src/web/app/common/views/components/time.vue
similarity index 100%
rename from src/web/app/common/tags/time.vue
rename to src/web/app/common/views/components/time.vue
diff --git a/src/web/app/common/tags/url-preview.vue b/src/web/app/common/views/components/url-preview.vue
similarity index 100%
rename from src/web/app/common/tags/url-preview.vue
rename to src/web/app/common/views/components/url-preview.vue
diff --git a/src/web/app/common/tags/url.vue b/src/web/app/common/views/components/url.vue
similarity index 100%
rename from src/web/app/common/tags/url.vue
rename to src/web/app/common/views/components/url.vue
diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index c0e1c0bd4..68b5f4cc9 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -17,105 +17,113 @@
 			<p class="c">{ _COPYRIGHT_ }</p>
 		</div>
 	</footer>
+	<modal name="signup">
+		<mk-signup/>
+	</modal>
 </div>
 </template>
 
-<style>
-	#wait {
-		right: auto;
-		left: 15px;
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	methods: {
+		signup() {
+			this.$modal.show('signup');
+		}
 	}
+});
+</script>
+
+<style>
+#wait {
+	right: auto;
+	left: 15px;
+}
 </style>
 
 <style lang="stylus" scoped>
-	.root
-		display flex
-		flex-direction column
-		flex 1
-		background #eee
-		$width = 1000px
+.root
+	display flex
+	flex-direction column
+	flex 1
+	background #eee
+	$width = 1000px
 
-		> main
-			display flex
-			flex 1
+	> main
+		display flex
+		flex 1
+		max-width $width
+		margin 0 auto
+		padding 80px 0 0 0
+
+		> div:first-child
+			margin 0 auto 0 0
+			width calc(100% - 500px)
+			color #777
+
+			> h1
+				margin 0
+				font-weight normal
+				font-variant small-caps
+				letter-spacing 12px
+
+			> p
+				margin 0.5em 0
+				line-height 2em
+
+			button
+				padding 8px 16px
+				font-size inherit
+
+			.signup
+				color $theme-color
+				border solid 2px $theme-color
+				border-radius 4px
+
+				&:focus
+					box-shadow 0 0 0 3px rgba($theme-color, 0.2)
+
+				&:hover
+					color $theme-color-foreground
+					background $theme-color
+
+				&:active
+					color $theme-color-foreground
+					background darken($theme-color, 10%)
+					border-color darken($theme-color, 10%)
+
+			.signin
+				&:focus
+					color #444
+
+				&:hover
+					color #444
+
+				&:active
+					color #333
+
+		> div:last-child
+			margin 0 0 0 auto
+
+	> footer
+		background #fff
+
+		*
+			color #fff !important
+			text-shadow 0 0 8px #000
+			font-weight bold
+
+		> div
 			max-width $width
 			margin 0 auto
-			padding 80px 0 0 0
+			padding 16px 0
+			text-align center
+			border-top solid 1px #fff
 
-			> div:first-child
-				margin 0 auto 0 0
-				width calc(100% - 500px)
-				color #777
-
-				> h1
-					margin 0
-					font-weight normal
-					font-variant small-caps
-					letter-spacing 12px
-
-				> p
-					margin 0.5em 0
-					line-height 2em
-
-				button
-					padding 8px 16px
-					font-size inherit
-
-				.signup
-					color $theme-color
-					border solid 2px $theme-color
-					border-radius 4px
-
-					&:focus
-						box-shadow 0 0 0 3px rgba($theme-color, 0.2)
-
-					&:hover
-						color $theme-color-foreground
-						background $theme-color
-
-					&:active
-						color $theme-color-foreground
-						background darken($theme-color, 10%)
-						border-color darken($theme-color, 10%)
-
-				.signin
-					&:focus
-						color #444
-
-					&:hover
-						color #444
-
-					&:active
-						color #333
-
-			> div:last-child
-				margin 0 0 0 auto
-
-		> footer
-			background #fff
-
-			*
-				color #fff !important
-				text-shadow 0 0 8px #000
-				font-weight bold
-
-			> div
-				max-width $width
-				margin 0 auto
-				padding 16px 0
-				text-align center
-				border-top solid 1px #fff
-
-				> .c
-					margin 0
-					line-height 64px
-					font-size 10px
+			> .c
+				margin 0
+				line-height 64px
+				font-size 10px
 
 </style>
-
-<script lang="ts">
-import Vue from 'vue'
-export default Vue.extend({
-
-})
-</script>
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 796a96694..20ea1df8b 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -9,8 +9,10 @@ declare const _HOST_: string;
 
 import Vue from 'vue';
 import VueRouter from 'vue-router';
+import VModal from 'vue-js-modal';
 
 Vue.use(VueRouter);
+Vue.use(VModal);
 
 import App from './app.vue';
 

From c869883d76455844e8d56ec4e863c6405489f897 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 17:01:32 +0900
Subject: [PATCH 030/286] wip

---
 .../app/common/views/components/signin.vue    |   2 +-
 .../app/common/views/components/signup.vue    | 452 ++++++++----------
 src/web/app/config.ts                         |  11 +
 src/web/app/desktop/views/pages/welcome.vue   |   2 +-
 src/web/app/init.ts                           |   3 +
 webpack/module/rules/fa.ts                    |   2 +-
 webpack/module/rules/i18n.ts                  |   2 +-
 7 files changed, 217 insertions(+), 257 deletions(-)
 create mode 100644 src/web/app/config.ts

diff --git a/src/web/app/common/views/components/signin.vue b/src/web/app/common/views/components/signin.vue
index 5ffc518b3..ee26110a4 100644
--- a/src/web/app/common/views/components/signin.vue
+++ b/src/web/app/common/views/components/signin.vue
@@ -13,7 +13,7 @@
 </form>
 </template>
 
-<script lang="ts">
+<script>
 import Vue from 'vue';
 
 export default Vue.extend({
diff --git a/src/web/app/common/views/components/signup.vue b/src/web/app/common/views/components/signup.vue
index 1734f7731..723555cdc 100644
--- a/src/web/app/common/views/components/signup.vue
+++ b/src/web/app/common/views/components/signup.vue
@@ -1,9 +1,9 @@
 <template>
-<form @submit.prevent="onSubmit" autocomplete="off">
+<form class="form" @submit.prevent="onSubmit" autocomplete="off">
 	<label class="username">
 		<p class="caption">%fa:at%%i18n:common.tags.mk-signup.username%</p>
 		<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @keyup="onChangeUsername"/>
-		<p class="profile-page-url-preview" v-if="refs.username.value != '' && username-state != 'invalidFormat' && username-state != 'minRange' && username-state != 'maxRange'">{ _URL_ + '/' + refs.username.value }</p>
+		<p class="profile-page-url-preview" v-if="username != '' && username-state != 'invalidFormat' && username-state != 'minRange' && username-state != 'maxRange'">{ _URL_ + '/' + refs.username.value }</p>
 		<p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:common.tags.mk-signup.checking%</p>
 		<p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.available%</p>
 		<p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.unavailable%</p>
@@ -30,7 +30,7 @@
 	</label>
 	<label class="recaptcha">
 		<p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:common.tags.mk-signup.recaptcha%</p>
-		<div v-if="recaptcha" class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" data-sitekey="recaptcha.site_key"></div>
+		<div v-if="recaptcha" class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" :data-sitekey="recaptchaSitekey"></div>
 	</label>
 	<label class="agree-tou">
 		<input name="agree-tou" type="checkbox" autocomplete="off" required/>
@@ -43,16 +43,98 @@
 <script lang="ts">
 import Vue from 'vue';
 const getPasswordStrength = require('syuilo-password-strength');
-import
-
-const aboutUrl = `${_DOCS_URL_}/${_LANG_}/tou`;
+import { docsUrl, lang, recaptchaSitekey } from '../../../config';
 
 export default Vue.extend({
-	methods: {
-		onSubmit() {
-
+	props: ['os'],
+	data() {
+		return {
+			username: '',
+			password: '',
+			retypedPassword: '',
+			touUrl: `${docsUrl}/${lang}/tou`,
+			recaptchaSitekey,
+			recaptchaed: false,
+			usernameState: null,
+			passwordStrength: '',
+			passwordRetypeState: null
 		}
 	},
+	methods: {
+		onChangeUsername() {
+			if (this.username == '') {
+				this.usernameState = null;
+				return;
+			}
+
+			const err =
+				!this.username.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
+				this.username.length < 3 ? 'min-range' :
+				this.username.length > 20 ? 'max-range' :
+				null;
+
+			if (err) {
+				this.usernameState = err;
+				return;
+			}
+
+			this.usernameState = 'wait';
+
+			this.os.api('username/available', {
+				username: this.username
+			}).then(result => {
+				this.usernameState = result.available ? 'ok' : 'unavailable';
+			}).catch(err => {
+				this.usernameState = 'error';
+			});
+		},
+		onChangePassword() {
+			if (this.password == '') {
+				this.passwordStrength = '';
+				return;
+			}
+
+			const strength = getPasswordStrength(this.password);
+			this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
+			(this.$refs.passwordMetar as any).style.width = `${strength * 100}%`;
+		},
+		onChangePasswordRetype() {
+			if (this.retypedPassword == '') {
+				this.passwordRetypeState = null;
+				return;
+			}
+
+			this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match';
+		},
+		onSubmit() {
+			this.os.api('signup', {
+				username: this.username,
+				password: this.password,
+				'g-recaptcha-response': (window as any).grecaptcha.getResponse()
+			}).then(() => {
+				this.os.api('signin', {
+					username: this.username,
+					password: this.password
+				}).then(() => {
+					location.href = '/';
+				});
+			}).catch(() => {
+				alert('%i18n:common.tags.mk-signup.some-error%');
+
+				(window as any).grecaptcha.reset();
+				this.recaptchaed = false;
+			});
+		}
+	},
+	created() {
+		(window as any).onRecaptchaed = () => {
+			this.recaptchaed = true;
+		};
+
+		(window as any).onRecaptchaExpired = () => {
+			this.recaptchaed = false;
+		};
+	},
 	mounted() {
 		const head = document.getElementsByTagName('head')[0];
 		const script = document.createElement('script');
@@ -63,269 +145,133 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-	:scope
+.form
+	min-width 302px
+
+	label
 		display block
-		min-width 302px
-		overflow hidden
+		margin 16px 0
 
-		> form
+		> .caption
+			margin 0 0 4px 0
+			color #828888
+			font-size 0.95em
 
-			label
+			> [data-fa]
+				margin-right 0.25em
+				color #96adac
+
+		> .info
+			display block
+			margin 4px 0
+			font-size 0.8em
+
+			> [data-fa]
+				margin-right 0.3em
+
+		&.username
+			.profile-page-url-preview
 				display block
-				margin 16px 0
+				margin 4px 8px 0 4px
+				font-size 0.8em
+				color #888
 
-				> .caption
-					margin 0 0 4px 0
-					color #828888
-					font-size 0.95em
+				&:empty
+					display none
 
-					> [data-fa]
-						margin-right 0.25em
-						color #96adac
+				&:not(:empty) + .info
+					margin-top 0
 
-				> .info
+		&.password
+			.meter
+				display block
+				margin-top 8px
+				width 100%
+				height 8px
+
+				&[data-strength='']
+					display none
+
+				&[data-strength='low']
+					> .value
+						background #d73612
+
+				&[data-strength='medium']
+					> .value
+						background #d7ca12
+
+				&[data-strength='high']
+					> .value
+						background #61bb22
+
+				> .value
 					display block
-					margin 4px 0
-					font-size 0.8em
+					width 0%
+					height 100%
+					background transparent
+					border-radius 4px
+					transition all 0.1s ease
 
-					> [data-fa]
-						margin-right 0.3em
+	[type=text], [type=password]
+		user-select text
+		display inline-block
+		cursor auto
+		padding 0 12px
+		margin 0
+		width 100%
+		line-height 44px
+		font-size 1em
+		color #333 !important
+		background #fff !important
+		outline none
+		border solid 1px rgba(0, 0, 0, 0.1)
+		border-radius 4px
+		box-shadow 0 0 0 114514px #fff inset
+		transition all .3s ease
 
-				&.username
-					.profile-page-url-preview
-						display block
-						margin 4px 8px 0 4px
-						font-size 0.8em
-						color #888
+		&:hover
+			border-color rgba(0, 0, 0, 0.2)
+			transition all .1s ease
 
-						&:empty
-							display none
+		&:focus
+			color $theme-color !important
+			border-color $theme-color
+			box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%)
+			transition all 0s ease
 
-						&:not(:empty) + .info
-							margin-top 0
+		&:disabled
+			opacity 0.5
 
-				&.password
-					.meter
-						display block
-						margin-top 8px
-						width 100%
-						height 8px
+	.agree-tou
+		padding 4px
+		border-radius 4px
 
-						&[data-strength='']
-							display none
+		&:hover
+			background #f4f4f4
 
-						&[data-strength='low']
-							> .value
-								background #d73612
+		&:active
+			background #eee
 
-						&[data-strength='medium']
-							> .value
-								background #d7ca12
+		&, *
+			cursor pointer
 
-						&[data-strength='high']
-							> .value
-								background #61bb22
+		p
+			display inline
+			color #555
 
-						> .value
-							display block
-							width 0%
-							height 100%
-							background transparent
-							border-radius 4px
-							transition all 0.1s ease
+	button
+		margin 0 0 32px 0
+		padding 16px
+		width 100%
+		font-size 1em
+		color #fff
+		background $theme-color
+		border-radius 3px
 
-			[type=text], [type=password]
-				user-select text
-				display inline-block
-				cursor auto
-				padding 0 12px
-				margin 0
-				width 100%
-				line-height 44px
-				font-size 1em
-				color #333 !important
-				background #fff !important
-				outline none
-				border solid 1px rgba(0, 0, 0, 0.1)
-				border-radius 4px
-				box-shadow 0 0 0 114514px #fff inset
-				transition all .3s ease
+		&:hover
+			background lighten($theme-color, 5%)
 
-				&:hover
-					border-color rgba(0, 0, 0, 0.2)
-					transition all .1s ease
-
-				&:focus
-					color $theme-color !important
-					border-color $theme-color
-					box-shadow 0 0 0 1024px #fff inset, 0 0 0 4px rgba($theme-color, 10%)
-					transition all 0s ease
-
-				&:disabled
-					opacity 0.5
-
-			.agree-tou
-				padding 4px
-				border-radius 4px
-
-				&:hover
-					background #f4f4f4
-
-				&:active
-					background #eee
-
-				&, *
-					cursor pointer
-
-				p
-					display inline
-					color #555
-
-			button
-				margin 0 0 32px 0
-				padding 16px
-				width 100%
-				font-size 1em
-				color #fff
-				background $theme-color
-				border-radius 3px
-
-				&:hover
-					background lighten($theme-color, 5%)
-
-				&:active
-					background darken($theme-color, 5%)
+		&:active
+			background darken($theme-color, 5%)
 
 </style>
-
-<script lang="typescript">
-	this.mixin('api');
-
-
-	this.usernameState = null;
-	this.passwordStrength = '';
-	this.passwordRetypeState = null;
-	this.recaptchaed = false;
-
-	this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/tou`;
-
-	window.onRecaptchaed = () => {
-		this.recaptchaed = true;
-		this.update();
-	};
-
-	window.onRecaptchaExpired = () => {
-		this.recaptchaed = false;
-		this.update();
-	};
-
-	this.on('mount', () => {
-		this.update({
-			recaptcha: {
-				site_key: _RECAPTCHA_SITEKEY_
-			}
-		});
-
-		const head = document.getElementsByTagName('head')[0];
-		const script = document.createElement('script');
-		script.setAttribute('src', 'https://www.google.com/recaptcha/api.js');
-		head.appendChild(script);
-	});
-
-	this.onChangeUsername = () => {
-		const username = this.$refs.username.value;
-
-		if (username == '') {
-			this.update({
-				usernameState: null
-			});
-			return;
-		}
-
-		const err =
-			!username.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
-			username.length < 3 ? 'min-range' :
-			username.length > 20 ? 'max-range' :
-			null;
-
-		if (err) {
-			this.update({
-				usernameState: err
-			});
-			return;
-		}
-
-		this.update({
-			usernameState: 'wait'
-		});
-
-		this.api('username/available', {
-			username: username
-		}).then(result => {
-			this.update({
-				usernameState: result.available ? 'ok' : 'unavailable'
-			});
-		}).catch(err => {
-			this.update({
-				usernameState: 'error'
-			});
-		});
-	};
-
-	this.onChangePassword = () => {
-		const password = this.$refs.password.value;
-
-		if (password == '') {
-			this.passwordStrength = '';
-			return;
-		}
-
-		const strength = getPasswordStrength(password);
-		this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
-		this.update();
-		this.$refs.passwordMetar.style.width = `${strength * 100}%`;
-	};
-
-	this.onChangePasswordRetype = () => {
-		const password = this.$refs.password.value;
-		const retypedPassword = this.$refs.passwordRetype.value;
-
-		if (retypedPassword == '') {
-			this.passwordRetypeState = null;
-			return;
-		}
-
-		this.passwordRetypeState = password == retypedPassword ? 'match' : 'not-match';
-	};
-
-	this.onsubmit = e => {
-		e.preventDefault();
-
-		const username = this.$refs.username.value;
-		const password = this.$refs.password.value;
-
-		const locker = document.body.appendChild(document.createElement('mk-locker'));
-
-		this.api('signup', {
-			username: username,
-			password: password,
-			'g-recaptcha-response': grecaptcha.getResponse()
-		}).then(() => {
-			this.api('signin', {
-				username: username,
-				password: password
-			}).then(() => {
-				location.href = '/';
-			});
-		}).catch(() => {
-			alert('%i18n:common.tags.mk-signup.some-error%');
-
-			grecaptcha.reset();
-			this.recaptchaed = false;
-
-			locker.parentNode.removeChild(locker);
-		});
-
-		return false;
-	};
-</script>
diff --git a/src/web/app/config.ts b/src/web/app/config.ts
new file mode 100644
index 000000000..8357cf6c7
--- /dev/null
+++ b/src/web/app/config.ts
@@ -0,0 +1,11 @@
+declare const _HOST_: string;
+declare const _URL_: string;
+declare const _DOCS_URL_: string;
+declare const _LANG_: string;
+declare const _RECAPTCHA_SITEKEY_: string;
+
+export const host = _HOST_;
+export const url = _URL_;
+export const docsUrl = _DOCS_URL_;
+export const lang = _LANG_;
+export const recaptchaSitekey = _RECAPTCHA_SITEKEY_;
diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index 68b5f4cc9..b47e82fae 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -18,7 +18,7 @@
 		</div>
 	</footer>
 	<modal name="signup">
-		<mk-signup/>
+		<mk-signup></mk-signup>
 	</modal>
 </div>
 </template>
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 20ea1df8b..3ae2a8adc 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -70,6 +70,9 @@ export default (callback: (os: MiOS, launch: () => Vue) => void, sw = false) =>
 		// アプリ基底要素マウント
 		document.body.innerHTML = '<div id="app"></div>';
 
+		// Register global components
+		require('./common/views/components');
+
 		const launch = () => {
 			return new Vue({
 				router: new VueRouter({
diff --git a/webpack/module/rules/fa.ts b/webpack/module/rules/fa.ts
index 891b78ece..267908923 100644
--- a/webpack/module/rules/fa.ts
+++ b/webpack/module/rules/fa.ts
@@ -7,7 +7,7 @@ import { pattern, replacement } from '../../../src/common/build/fa';
 
 export default () => ({
 	enforce: 'pre',
-	test: /\.(tag|js|ts)$/,
+	test: /\.(vue|js|ts)$/,
 	exclude: /node_modules/,
 	loader: StringReplacePlugin.replace({
 		replacements: [{
diff --git a/webpack/module/rules/i18n.ts b/webpack/module/rules/i18n.ts
index 7261548be..f8063a311 100644
--- a/webpack/module/rules/i18n.ts
+++ b/webpack/module/rules/i18n.ts
@@ -10,7 +10,7 @@ export default lang => {
 
 	return {
 		enforce: 'pre',
-		test: /\.(tag|js|ts)$/,
+		test: /\.(vue|js|ts)$/,
 		exclude: /node_modules/,
 		loader: StringReplacePlugin.replace({
 			replacements: [{

From 7664354187a3847484f2a635e2eecd8a5be0b9f1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 10 Feb 2018 19:57:37 +0900
Subject: [PATCH 031/286] wip

---
 .../app/common/views/components/signup.vue    | 10 ++++-----
 src/web/app/desktop/views/pages/welcome.vue   | 21 +++++++++++++++----
 2 files changed, 22 insertions(+), 9 deletions(-)

diff --git a/src/web/app/common/views/components/signup.vue b/src/web/app/common/views/components/signup.vue
index 723555cdc..5bb464785 100644
--- a/src/web/app/common/views/components/signup.vue
+++ b/src/web/app/common/views/components/signup.vue
@@ -1,5 +1,5 @@
 <template>
-<form class="form" @submit.prevent="onSubmit" autocomplete="off">
+<form class="mk-signup" @submit.prevent="onSubmit" autocomplete="off">
 	<label class="username">
 		<p class="caption">%fa:at%%i18n:common.tags.mk-signup.username%</p>
 		<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @keyup="onChangeUsername"/>
@@ -24,7 +24,7 @@
 	</label>
 	<label class="retype-password">
 		<p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%(%i18n:common.tags.mk-signup.retype%)</p>
-		<input v-model="passwordRetype" type="password" placeholder="%i18n:common.tags.mk-signup.retype-placeholder%" autocomplete="off" required @keyup="onChangePasswordRetype"/>
+		<input v-model="retypedPassword" type="password" placeholder="%i18n:common.tags.mk-signup.retype-placeholder%" autocomplete="off" required @keyup="onChangePasswordRetype"/>
 		<p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.password-matched%</p>
 		<p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.password-not-matched%</p>
 	</label>
@@ -145,12 +145,12 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.form
+.mk-signup
 	min-width 302px
 
 	label
 		display block
-		margin 16px 0
+		margin 0 0 16px 0
 
 		> .caption
 			margin 0 0 4px 0
@@ -260,7 +260,7 @@ export default Vue.extend({
 			color #555
 
 	button
-		margin 0 0 32px 0
+		margin 0
 		padding 16px
 		width 100%
 		font-size 1em
diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index b47e82fae..234239f6e 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="root">
+<div class="mk-welcome">
 	<main>
 		<div>
 			<h1>Share<br>Everything!</h1>
@@ -17,8 +17,9 @@
 			<p class="c">{ _COPYRIGHT_ }</p>
 		</div>
 	</footer>
-	<modal name="signup">
-		<mk-signup></mk-signup>
+	<modal name="signup" width="500px" height="auto" scrollable>
+		<header :class="$style.signupFormHeader">新規登録</header>
+		<mk-signup :class="$style.signupForm"></mk-signup>
 	</modal>
 </div>
 </template>
@@ -43,7 +44,7 @@ export default Vue.extend({
 </style>
 
 <style lang="stylus" scoped>
-.root
+.mk-welcome
 	display flex
 	flex-direction column
 	flex 1
@@ -127,3 +128,15 @@ export default Vue.extend({
 				font-size 10px
 
 </style>
+
+<style lang="stylus" module>
+.signupForm
+	padding 24px 48px 48px 48px
+
+.signupFormHeader
+	padding 48px 0 12px 0
+	margin: 0 48px
+	font-size 1.5em
+	color #777
+	border-bottom solid 1px #eee
+</style>

From 92826c406a5109a054aadf473d078a10b1f4ecd7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 12:08:43 +0900
Subject: [PATCH 032/286] wip

---
 locales/index.ts                              |  2 +-
 src/web/app/common/-tags/forkit.tag           | 40 -------------------
 .../app/common/views/components/forkit.vue    | 40 +++++++++++++++++++
 src/web/app/common/views/components/index.ts  |  2 +
 .../app/common/views/components/signin.vue    | 12 +++---
 .../app/common/views/components/signup.vue    | 30 +++++++++-----
 src/web/app/config.ts                         | 10 +++++
 src/web/app/desktop/script.ts                 | 16 ++++----
 src/web/app/desktop/views/pages/index.vue     |  3 +-
 src/web/app/desktop/views/pages/welcome.vue   | 19 ++++++++-
 src/web/app/init.ts                           |  7 +++-
 src/web/app/mobile/script.ts                  |  6 +--
 12 files changed, 113 insertions(+), 74 deletions(-)
 delete mode 100644 src/web/app/common/-tags/forkit.tag
 create mode 100644 src/web/app/common/views/components/forkit.vue

diff --git a/locales/index.ts b/locales/index.ts
index 0593af366..ced3b4cb3 100644
--- a/locales/index.ts
+++ b/locales/index.ts
@@ -11,7 +11,7 @@ const loadLang = lang => yaml.safeLoad(
 const native = loadLang('ja');
 
 const langs = {
-	'en': loadLang('en'),
+	//'en': loadLang('en'),
 	'ja': native
 };
 
diff --git a/src/web/app/common/-tags/forkit.tag b/src/web/app/common/-tags/forkit.tag
deleted file mode 100644
index 6a8d06e56..000000000
--- a/src/web/app/common/-tags/forkit.tag
+++ /dev/null
@@ -1,40 +0,0 @@
-<mk-forkit><a href="https://github.com/syuilo/misskey" target="_blank" title="%i18n:common.tags.mk-forkit.open-github-link%" aria-label="%i18n:common.tags.mk-forkit.open-github-link%">
-		<svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="aria-hidden">
-			<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
-			<path class="octo-arm" d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor"></path>
-			<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor"></path>
-		</svg></a>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			position absolute
-			top 0
-			right 0
-
-			> a
-				display block
-
-				> svg
-					display block
-					//fill #151513
-					//color #fff
-					fill $theme-color
-					color $theme-color-foreground
-
-			.octo-arm
-				transform-origin 130px 106px
-
-			&:hover
-				.octo-arm
-					animation octocat-wave 560ms ease-in-out
-
-			@keyframes octocat-wave
-				0%, 100%
-					transform rotate(0)
-				20%, 60%
-					transform rotate(-25deg)
-				40%, 80%
-					transform rotate(10deg)
-
-	</style>
-</mk-forkit>
diff --git a/src/web/app/common/views/components/forkit.vue b/src/web/app/common/views/components/forkit.vue
new file mode 100644
index 000000000..54fc011d1
--- /dev/null
+++ b/src/web/app/common/views/components/forkit.vue
@@ -0,0 +1,40 @@
+<template>
+<a class="a" href="https://github.com/syuilo/misskey" target="_blank" title="%i18n:common.tags.mk-forkit.open-github-link%" aria-label="%i18n:common.tags.mk-forkit.open-github-link%">
+	<svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="aria-hidden">
+		<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
+		<path class="octo-arm" d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor"></path>
+		<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor"></path>
+	</svg>
+</a>
+</template>
+
+<style lang="stylus" scoped>
+	.a
+		display block
+		position absolute
+		top 0
+		right 0
+
+		> svg
+			display block
+			//fill #151513
+			//color #fff
+			fill $theme-color
+			color $theme-color-foreground
+
+			.octo-arm
+				transform-origin 130px 106px
+
+		&:hover
+			.octo-arm
+				animation octocat-wave 560ms ease-in-out
+
+		@keyframes octocat-wave
+			0%, 100%
+				transform rotate(0)
+			20%, 60%
+				transform rotate(-25deg)
+			40%, 80%
+				transform rotate(10deg)
+
+</style>
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index b1c5df819..968d5d7a9 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -2,6 +2,8 @@ import Vue from 'vue';
 
 import signin from './signin.vue';
 import signup from './signup.vue';
+import forkit from './forkit.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
+Vue.component('mk-forkit', forkit);
diff --git a/src/web/app/common/views/components/signin.vue b/src/web/app/common/views/components/signin.vue
index ee26110a4..fe28ddd24 100644
--- a/src/web/app/common/views/components/signin.vue
+++ b/src/web/app/common/views/components/signin.vue
@@ -13,20 +13,22 @@
 </form>
 </template>
 
-<script>
+<script lang="ts">
 import Vue from 'vue';
 
 export default Vue.extend({
-	props: ['os'],
 	data() {
 		return {
 			signing: false,
-			user: null
+			user: null,
+			username: '',
+			password: '',
+			token: ''
 		};
 	},
 	methods: {
 		onUsernameChange() {
-			this.os.api('users/show', {
+			this.$root.$data.os.api('users/show', {
 				username: this.username
 			}).then(user => {
 				this.user = user;
@@ -35,7 +37,7 @@ export default Vue.extend({
 		onSubmit() {
 			this.signing = true;
 
-			this.os.api('signin', {
+			this.$root.$data.os.api('signin', {
 				username: this.username,
 				password: this.password,
 				token: this.user && this.user.two_factor_enabled ? this.token : undefined
diff --git a/src/web/app/common/views/components/signup.vue b/src/web/app/common/views/components/signup.vue
index 5bb464785..34d17ef0e 100644
--- a/src/web/app/common/views/components/signup.vue
+++ b/src/web/app/common/views/components/signup.vue
@@ -2,8 +2,8 @@
 <form class="mk-signup" @submit.prevent="onSubmit" autocomplete="off">
 	<label class="username">
 		<p class="caption">%fa:at%%i18n:common.tags.mk-signup.username%</p>
-		<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @keyup="onChangeUsername"/>
-		<p class="profile-page-url-preview" v-if="username != '' && username-state != 'invalidFormat' && username-state != 'minRange' && username-state != 'maxRange'">{ _URL_ + '/' + refs.username.value }</p>
+		<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]{3,20}$" placeholder="a~z、A~Z、0~9、-" autocomplete="off" required @input="onChangeUsername"/>
+		<p class="profile-page-url-preview" v-if="shouldShowProfileUrl">{{ `${url}/${username}` }}</p>
 		<p class="info" v-if="usernameState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%%i18n:common.tags.mk-signup.checking%</p>
 		<p class="info" v-if="usernameState == 'ok'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.available%</p>
 		<p class="info" v-if="usernameState == 'unavailable'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.unavailable%</p>
@@ -14,8 +14,8 @@
 	</label>
 	<label class="password">
 		<p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%</p>
-		<input v-model="password" type="password" placeholder="%i18n:common.tags.mk-signup.password-placeholder%" autocomplete="off" required @keyup="onChangePassword"/>
-		<div class="meter" v-if="passwordStrength != ''" :data-strength="passwordStrength">
+		<input v-model="password" type="password" placeholder="%i18n:common.tags.mk-signup.password-placeholder%" autocomplete="off" required @input="onChangePassword"/>
+		<div class="meter" v-show="passwordStrength != ''" :data-strength="passwordStrength">
 			<div class="value" ref="passwordMetar"></div>
 		</div>
 		<p class="info" v-if="passwordStrength == 'low'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.weak-password%</p>
@@ -24,13 +24,13 @@
 	</label>
 	<label class="retype-password">
 		<p class="caption">%fa:lock%%i18n:common.tags.mk-signup.password%(%i18n:common.tags.mk-signup.retype%)</p>
-		<input v-model="retypedPassword" type="password" placeholder="%i18n:common.tags.mk-signup.retype-placeholder%" autocomplete="off" required @keyup="onChangePasswordRetype"/>
+		<input v-model="retypedPassword" type="password" placeholder="%i18n:common.tags.mk-signup.retype-placeholder%" autocomplete="off" required @input="onChangePasswordRetype"/>
 		<p class="info" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5">%fa:check .fw%%i18n:common.tags.mk-signup.password-matched%</p>
 		<p class="info" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161">%fa:exclamation-triangle .fw%%i18n:common.tags.mk-signup.password-not-matched%</p>
 	</label>
 	<label class="recaptcha">
 		<p class="caption"><template v-if="recaptchaed">%fa:toggle-on%</template><template v-if="!recaptchaed">%fa:toggle-off%</template>%i18n:common.tags.mk-signup.recaptcha%</p>
-		<div v-if="recaptcha" class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" :data-sitekey="recaptchaSitekey"></div>
+		<div class="g-recaptcha" data-callback="onRecaptchaed" data-expired-callback="onRecaptchaExpired" :data-sitekey="recaptchaSitekey"></div>
 	</label>
 	<label class="agree-tou">
 		<input name="agree-tou" type="checkbox" autocomplete="off" required/>
@@ -43,15 +43,15 @@
 <script lang="ts">
 import Vue from 'vue';
 const getPasswordStrength = require('syuilo-password-strength');
-import { docsUrl, lang, recaptchaSitekey } from '../../../config';
+import { url, docsUrl, lang, recaptchaSitekey } from '../../../config';
 
 export default Vue.extend({
-	props: ['os'],
 	data() {
 		return {
 			username: '',
 			password: '',
 			retypedPassword: '',
+			url,
 			touUrl: `${docsUrl}/${lang}/tou`,
 			recaptchaSitekey,
 			recaptchaed: false,
@@ -60,6 +60,14 @@ export default Vue.extend({
 			passwordRetypeState: null
 		}
 	},
+	computed: {
+		shouldShowProfileUrl(): boolean {
+			return (this.username != '' &&
+				this.usernameState != 'invalid-format' &&
+				this.usernameState != 'min-range' &&
+				this.usernameState != 'max-range');
+		}
+	},
 	methods: {
 		onChangeUsername() {
 			if (this.username == '') {
@@ -80,7 +88,7 @@ export default Vue.extend({
 
 			this.usernameState = 'wait';
 
-			this.os.api('username/available', {
+			this.$root.$data.os.api('username/available', {
 				username: this.username
 			}).then(result => {
 				this.usernameState = result.available ? 'ok' : 'unavailable';
@@ -107,12 +115,12 @@ export default Vue.extend({
 			this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match';
 		},
 		onSubmit() {
-			this.os.api('signup', {
+			this.$root.$data.os.api('signup', {
 				username: this.username,
 				password: this.password,
 				'g-recaptcha-response': (window as any).grecaptcha.getResponse()
 			}).then(() => {
-				this.os.api('signin', {
+				this.$root.$data.os.api('signin', {
 					username: this.username,
 					password: this.password
 				}).then(() => {
diff --git a/src/web/app/config.ts b/src/web/app/config.ts
index 8357cf6c7..a54a99b4c 100644
--- a/src/web/app/config.ts
+++ b/src/web/app/config.ts
@@ -1,11 +1,21 @@
 declare const _HOST_: string;
 declare const _URL_: string;
+declare const _API_URL_: string;
 declare const _DOCS_URL_: string;
 declare const _LANG_: string;
 declare const _RECAPTCHA_SITEKEY_: string;
+declare const _SW_PUBLICKEY_: string;
+declare const _THEME_COLOR_: string;
+declare const _COPYRIGHT_: string;
+declare const _VERSION_: string;
 
 export const host = _HOST_;
 export const url = _URL_;
+export const apiUrl = _API_URL_;
 export const docsUrl = _DOCS_URL_;
 export const lang = _LANG_;
 export const recaptchaSitekey = _RECAPTCHA_SITEKEY_;
+export const swPublickey = _SW_PUBLICKEY_;
+export const themeColor = _THEME_COLOR_;
+export const copyright = _COPYRIGHT_;
+export const version = _VERSION_;
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index e4e5f1914..d6ad0202d 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -15,12 +15,17 @@ import MkIndex from './views/pages/index.vue';
 /**
  * init
  */
-init(async (os, launch) => {
+init(async (launch) => {
 	/**
 	 * Fuck AD Block
 	 */
 	fuckAdBlock();
 
+	// Register components
+	require('./views/components');
+
+	const app = launch();
+
 	/**
 	 * Init Notification
 	 */
@@ -31,17 +36,12 @@ init(async (os, launch) => {
 		}
 
 		if ((Notification as any).permission == 'granted') {
-			registerNotifications(os.stream);
+			registerNotifications(app.$data.os.stream);
 		}
 	}
 
-	// Register components
-	require('./views/components');
-
-	const app = launch();
-
 	app.$router.addRoutes([{
-		path: '/', component: MkIndex, props: { os }
+		path: '/', component: MkIndex
 	}]);
 }, true);
 
diff --git a/src/web/app/desktop/views/pages/index.vue b/src/web/app/desktop/views/pages/index.vue
index dbe77e081..6377b6a27 100644
--- a/src/web/app/desktop/views/pages/index.vue
+++ b/src/web/app/desktop/views/pages/index.vue
@@ -1,5 +1,5 @@
 <template>
-	<component v-bind:is="os.isSignedIn ? 'home' : 'welcome'"></component>
+	<component v-bind:is="$root.$data.os.isSignedIn ? 'home' : 'welcome'"></component>
 </template>
 
 <script lang="ts">
@@ -8,7 +8,6 @@ import HomeView from './home.vue';
 import WelcomeView from './welcome.vue';
 
 export default Vue.extend({
-	props: ['os'],
 	components: {
 		home: HomeView,
 		welcome: WelcomeView
diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index 234239f6e..a4202de04 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -19,7 +19,11 @@
 	</footer>
 	<modal name="signup" width="500px" height="auto" scrollable>
 		<header :class="$style.signupFormHeader">新規登録</header>
-		<mk-signup :class="$style.signupForm"></mk-signup>
+		<mk-signup :class="$style.signupForm"/>
+	</modal>
+	<modal name="signin" width="500px" height="auto" scrollable>
+		<header :class="$style.signinFormHeader">ログイン</header>
+		<mk-signin :class="$style.signinForm"/>
 	</modal>
 </div>
 </template>
@@ -31,6 +35,9 @@ export default Vue.extend({
 	methods: {
 		signup() {
 			this.$modal.show('signup');
+		},
+		signin() {
+			this.$modal.show('signin');
 		}
 	}
 });
@@ -139,4 +146,14 @@ export default Vue.extend({
 	font-size 1.5em
 	color #777
 	border-bottom solid 1px #eee
+
+.signinForm
+	padding 24px 48px 48px 48px
+
+.signinFormHeader
+	padding 48px 0 12px 0
+	margin: 0 48px
+	font-size 1.5em
+	color #777
+	border-bottom solid 1px #eee
 </style>
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 3ae2a8adc..dfb1e96b8 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -63,7 +63,7 @@ if (localStorage.getItem('should-refresh') == 'true') {
 }
 
 // MiOSを初期化してコールバックする
-export default (callback: (os: MiOS, launch: () => Vue) => void, sw = false) => {
+export default (callback: (launch: () => Vue) => void, sw = false) => {
 	const mios = new MiOS(sw);
 
 	mios.init(() => {
@@ -75,6 +75,9 @@ export default (callback: (os: MiOS, launch: () => Vue) => void, sw = false) =>
 
 		const launch = () => {
 			return new Vue({
+				data: {
+					os: mios
+				},
 				router: new VueRouter({
 					mode: 'history'
 				}),
@@ -83,7 +86,7 @@ export default (callback: (os: MiOS, launch: () => Vue) => void, sw = false) =>
 		};
 
 		try {
-			callback(mios, launch);
+			callback(launch);
 		} catch (e) {
 			panic(e);
 		}
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index 4dfff8f72..f7129c553 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -7,16 +7,14 @@ import './style.styl';
 
 require('./tags');
 import init from '../init';
-import route from './router';
-import MiOS from '../common/mios';
 
 /**
  * init
  */
-init((mios: MiOS) => {
+init((launch) => {
 	// http://qiita.com/junya/items/3ff380878f26ca447f85
 	document.body.setAttribute('ontouchstart', '');
 
 	// Start routing
-	route(mios);
+	//route(mios);
 }, true);

From dab2a5dd1cd57ae11c67c1e1b3f33955761630f2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 12:42:02 +0900
Subject: [PATCH 033/286] wip

---
 src/web/app/common/-tags/nav-links.tag        | 10 ------
 src/web/app/common/views/components/index.ts  |  2 ++
 src/web/app/common/views/components/nav.vue   | 35 +++++++++++++++++++
 .../app/common/views/components/signin.vue    |  9 ++---
 src/web/app/config.ts                         |  6 ++++
 src/web/app/desktop/views/pages/welcome.vue   | 26 ++++++++------
 6 files changed, 61 insertions(+), 27 deletions(-)
 delete mode 100644 src/web/app/common/-tags/nav-links.tag
 create mode 100644 src/web/app/common/views/components/nav.vue

diff --git a/src/web/app/common/-tags/nav-links.tag b/src/web/app/common/-tags/nav-links.tag
deleted file mode 100644
index 3f2613c16..000000000
--- a/src/web/app/common/-tags/nav-links.tag
+++ /dev/null
@@ -1,10 +0,0 @@
-<mk-nav-links>
-	<a href={ aboutUrl }>%i18n:common.tags.mk-nav-links.about%</a><i>・</i><a href={ _STATS_URL_ }>%i18n:common.tags.mk-nav-links.stats%</a><i>・</i><a href={ _STATUS_URL_ }>%i18n:common.tags.mk-nav-links.status%</a><i>・</i><a href="http://zawazawa.jp/misskey/">%i18n:common.tags.mk-nav-links.wiki%</a><i>・</i><a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:common.tags.mk-nav-links.donors%</a><i>・</i><a href="https://github.com/syuilo/misskey">%i18n:common.tags.mk-nav-links.repository%</a><i>・</i><a href={ _DEV_URL_ }>%i18n:common.tags.mk-nav-links.develop%</a><i>・</i><a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on %fa:B twitter%</a>
-	<style lang="stylus" scoped>
-		:scope
-			display inline
-	</style>
-	<script lang="typescript">
-		this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/about`;
-	</script>
-</mk-nav-links>
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 968d5d7a9..9097c3081 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -3,7 +3,9 @@ import Vue from 'vue';
 import signin from './signin.vue';
 import signup from './signup.vue';
 import forkit from './forkit.vue';
+import nav from './nav.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
 Vue.component('mk-forkit', forkit);
+Vue.component('mk-nav', nav);
diff --git a/src/web/app/common/views/components/nav.vue b/src/web/app/common/views/components/nav.vue
new file mode 100644
index 000000000..6cd86216c
--- /dev/null
+++ b/src/web/app/common/views/components/nav.vue
@@ -0,0 +1,35 @@
+<template>
+<span>
+	<a :href="aboutUrl">%i18n:common.tags.mk-nav-links.about%</a>
+	<i>・</i>
+	<a :href="statsUrl">%i18n:common.tags.mk-nav-links.stats%</a>
+	<i>・</i>
+	<a :href="statusUrl">%i18n:common.tags.mk-nav-links.status%</a>
+	<i>・</i>
+	<a href="http://zawazawa.jp/misskey/">%i18n:common.tags.mk-nav-links.wiki%</a>
+	<i>・</i>
+	<a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:common.tags.mk-nav-links.donors%</a>
+	<i>・</i>
+	<a href="https://github.com/syuilo/misskey">%i18n:common.tags.mk-nav-links.repository%</a>
+	<i>・</i>
+	<a :href="devUrl">%i18n:common.tags.mk-nav-links.develop%</a>
+	<i>・</i>
+	<a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on %fa:B twitter%</a>
+</span>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { docsUrl, statsUrl, statusUrl, devUrl, lang } from '../../../config';
+
+export default Vue.extend({
+	data() {
+		return {
+			aboutUrl: `${docsUrl}/${lang}/about`,
+			statsUrl,
+			statusUrl,
+			devUrl
+		}
+	}
+});
+</script>
diff --git a/src/web/app/common/views/components/signin.vue b/src/web/app/common/views/components/signin.vue
index fe28ddd24..989c01705 100644
--- a/src/web/app/common/views/components/signin.vue
+++ b/src/web/app/common/views/components/signin.vue
@@ -1,5 +1,5 @@
 <template>
-<form class="form" :class="{ signing: signing }" @submit.prevent="onSubmit">
+<form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit">
 	<label class="user-name">
 		<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]+$" placeholder="%i18n:common.tags.mk-signin.username%" autofocus required @change="onUsernameChange"/>%fa:at%
 	</label>
@@ -9,7 +9,7 @@
 	<label class="token" v-if="user && user.two_factor_enabled">
 		<input v-model="token" type="number" placeholder="%i18n:common.tags.mk-signin.token%" required/>%fa:lock%
 	</label>
-	<button type="submit" disabled={ signing }>{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }</button>
+	<button type="submit" :disabled="signing">{{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }}</button>
 </form>
 </template>
 
@@ -53,10 +53,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.form
-	display block
-	z-index 2
-
+.mk-signin
 	&.signing
 		&, *
 			cursor wait !important
diff --git a/src/web/app/config.ts b/src/web/app/config.ts
index a54a99b4c..25381ecce 100644
--- a/src/web/app/config.ts
+++ b/src/web/app/config.ts
@@ -2,6 +2,9 @@ declare const _HOST_: string;
 declare const _URL_: string;
 declare const _API_URL_: string;
 declare const _DOCS_URL_: string;
+declare const _STATS_URL_: string;
+declare const _STATUS_URL_: string;
+declare const _DEV_URL_: string;
 declare const _LANG_: string;
 declare const _RECAPTCHA_SITEKEY_: string;
 declare const _SW_PUBLICKEY_: string;
@@ -13,6 +16,9 @@ export const host = _HOST_;
 export const url = _URL_;
 export const apiUrl = _API_URL_;
 export const docsUrl = _DOCS_URL_;
+export const statsUrl = _STATS_URL_;
+export const statusUrl = _STATUS_URL_;
+export const devUrl = _DEV_URL_;
 export const lang = _LANG_;
 export const recaptchaSitekey = _RECAPTCHA_SITEKEY_;
 export const swPublickey = _SW_PUBLICKEY_;
diff --git a/src/web/app/desktop/views/pages/welcome.vue b/src/web/app/desktop/views/pages/welcome.vue
index a4202de04..f359ce008 100644
--- a/src/web/app/desktop/views/pages/welcome.vue
+++ b/src/web/app/desktop/views/pages/welcome.vue
@@ -13,8 +13,8 @@
 	<mk-forkit/>
 	<footer>
 		<div>
-			<mk-nav-links/>
-			<p class="c">{ _COPYRIGHT_ }</p>
+			<mk-nav :class="$style.nav"/>
+			<p class="c">{{ copyright }}</p>
 		</div>
 	</footer>
 	<modal name="signup" width="500px" height="auto" scrollable>
@@ -30,8 +30,14 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import { copyright } from '../../../config';
 
 export default Vue.extend({
+	data() {
+		return {
+			copyright
+		};
+	},
 	methods: {
 		signup() {
 			this.$modal.show('signup');
@@ -115,23 +121,17 @@ export default Vue.extend({
 			margin 0 0 0 auto
 
 	> footer
+		color #666
 		background #fff
 
-		*
-			color #fff !important
-			text-shadow 0 0 8px #000
-			font-weight bold
-
 		> div
 			max-width $width
 			margin 0 auto
-			padding 16px 0
+			padding 42px 0
 			text-align center
-			border-top solid 1px #fff
 
 			> .c
-				margin 0
-				line-height 64px
+				margin 16px 0 0 0
 				font-size 10px
 
 </style>
@@ -156,4 +156,8 @@ export default Vue.extend({
 	font-size 1.5em
 	color #777
 	border-bottom solid 1px #eee
+
+.nav
+	a
+		color #666
 </style>

From cd5786d7fb15f48da353939f8d52f714069fdb01 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 13:02:35 +0900
Subject: [PATCH 034/286] wip

---
 .../desktop/views/{ => components}/home.vue   | 334 +++++++++---------
 src/web/app/desktop/views/components/index.ts |   2 +
 2 files changed, 166 insertions(+), 170 deletions(-)
 rename src/web/app/desktop/views/{ => components}/home.vue (57%)

diff --git a/src/web/app/desktop/views/home.vue b/src/web/app/desktop/views/components/home.vue
similarity index 57%
rename from src/web/app/desktop/views/home.vue
rename to src/web/app/desktop/views/components/home.vue
index d054127da..987f272a0 100644
--- a/src/web/app/desktop/views/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -1,11 +1,11 @@
 <template>
-<div :data-customize="customize">
+<div class="mk-home" :data-customize="customize">
 	<div class="customize" v-if="customize">
 		<a href="/">%fa:check%完了</a>
 		<div>
 			<div class="adder">
 				<p>ウィジェットを追加:</p>
-				<select ref="widgetSelector">
+				<select v-model="widgetAdderSelected">
 					<option value="profile">プロフィール</option>
 					<option value="calendar">カレンダー</option>
 					<option value="timemachine">カレンダー(タイムマシン)</option>
@@ -40,11 +40,11 @@
 		<div class="left">
 			<div ref="left" data-place="left">
 				<template v-for="widget in leftWidgets">
-					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu="onWidgetContextmenu.stop.prevent(widget.id)">
-						<component :is="widget.name" :widget="widget" :ref="widget.id"></component>
+					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
+						<component :is="widget.name" :widget="widget" :ref="widget.id"/>
 					</div>
 					<template v-else>
-						<component :is="widget.name" :key="widget.id" :widget="widget" :ref="widget.id"></component>
+						<component :is="widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
 					</template>
 				</template>
 			</div>
@@ -52,25 +52,25 @@
 		<main ref="main">
 			<div class="maintop" ref="maintop" data-place="main" v-if="customize">
 				<template v-for="widget in centerWidgets">
-					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu="onWidgetContextmenu.stop.prevent(widget.id)">
-						<component :is="widget.name" :widget="widget" :ref="widget.id"></component>
+					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
+						<component :is="widget.name" :widget="widget" :ref="widget.id"/>
 					</div>
 					<template v-else>
-						<component :is="widget.name" :key="widget.id" :widget="widget" :ref="widget.id"></component>
+						<component :is="widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
 					</template>
 				</template>
 			</div>
-			<mk-timeline-home-widget ref="tl" v-on:loaded="onTlLoaded" v-if="mode == 'timeline'"/>
-			<mk-mentions-home-widget ref="tl" v-on:loaded="onTlLoaded" v-if="mode == 'mentions'"/>
+			<mk-timeline-home-widget ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
+			<mk-mentions-home-widget ref="tl" @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
 		</main>
 		<div class="right">
 			<div ref="right" data-place="right">
 				<template v-for="widget in rightWidgets">
-					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu="onWidgetContextmenu.stop.prevent(widget.id)">
-						<component :is="widget.name" :widget="widget" :ref="widget.id"></component>
+					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
+						<component :is="widget.name" :widget="widget" :ref="widget.id"/>
 					</div>
 					<template v-else>
-						<component :is="widget.name" :key="widget.id" :widget="widget" :ref="widget.id"></component>
+						<component :is="widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
 					</template>
 				</template>
 			</div>
@@ -80,10 +80,11 @@
 </template>
 
 <script lang="typescript">
+import Vue from 'vue';
 import uuid from 'uuid';
 import Sortable from 'sortablejs';
 
-export default {
+export default Vue.extend({
 	props: {
 		customize: Boolean,
 		mode: {
@@ -94,12 +95,13 @@ export default {
 	data() {
 		return {
 			home: [],
-			bakedHomeData: null
+			bakedHomeData: null,
+			widgetAdderSelected: null
 		};
 	},
 	methods: {
 		bakeHomeData() {
-			return JSON.stringify(this.I.client_settings.home);
+			return JSON.stringify(this.$root.$data.os.i.client_settings.home);
 		},
 		onTlLoaded() {
 			this.$emit('loaded');
@@ -111,94 +113,86 @@ export default {
 			}
 		},
 		onWidgetContextmenu(widgetId) {
-			this.$refs[widgetId].func();
+			(this.$refs[widgetId] as any).func();
 		},
 		addWidget() {
 			const widget = {
-				name: this.$refs.widgetSelector.options[this.$refs.widgetSelector.selectedIndex].value,
+				name: this.widgetAdderSelected,
 				id: uuid(),
 				place: 'left',
 				data: {}
 			};
 
-			this.I.client_settings.home.unshift(widget);
+			this.$root.$data.os.i.client_settings.home.unshift(widget);
 
 			this.saveHome();
 		},
 		saveHome() {
-			/*const data = [];
+			const data = [];
 
-			Array.from(this.$refs.left.children).forEach(el => {
+			Array.from((this.$refs.left as Element).children).forEach(el => {
 				const id = el.getAttribute('data-widget-id');
-				const widget = this.I.client_settings.home.find(w => w.id == id);
+				const widget = this.$root.$data.os.i.client_settings.home.find(w => w.id == id);
 				widget.place = 'left';
 				data.push(widget);
 			});
 
-			Array.from(this.$refs.right.children).forEach(el => {
+			Array.from((this.$refs.right as Element).children).forEach(el => {
 				const id = el.getAttribute('data-widget-id');
-				const widget = this.I.client_settings.home.find(w => w.id == id);
+				const widget = this.$root.$data.os.i.client_settings.home.find(w => w.id == id);
 				widget.place = 'right';
 				data.push(widget);
 			});
 
-			Array.from(this.$refs.maintop.children).forEach(el => {
+			Array.from((this.$refs.maintop as Element).children).forEach(el => {
 				const id = el.getAttribute('data-widget-id');
-				const widget = this.I.client_settings.home.find(w => w.id == id);
+				const widget = this.$root.$data.os.i.client_settings.home.find(w => w.id == id);
 				widget.place = 'main';
 				data.push(widget);
 			});
 
-			this.api('i/update_home', {
+			this.$root.$data.os.api('i/update_home', {
 				home: data
-			}).then(() => {
-				this.I.update();
-			});*/
+			});
 		}
 	},
 	computed: {
-		leftWidgets() {
-			return this.I.client_settings.home.filter(w => w.place == 'left');
+		leftWidgets(): any {
+			return this.$root.$data.os.i.client_settings.home.filter(w => w.place == 'left');
 		},
-		centerWidgets() {
-			return this.I.client_settings.home.filter(w => w.place == 'center');
+		centerWidgets(): any {
+			return this.$root.$data.os.i.client_settings.home.filter(w => w.place == 'center');
 		},
-		rightWidgets() {
-			return this.I.client_settings.home.filter(w => w.place == 'right');
+		rightWidgets(): any {
+			return this.$root.$data.os.i.client_settings.home.filter(w => w.place == 'right');
 		}
 	},
 	created() {
 		this.bakedHomeData = this.bakeHomeData();
 	},
 	mounted() {
-		this.I.on('refreshed', this.onMeRefreshed);
+		this.$root.$data.os.i.on('refreshed', this.onMeRefreshed);
 
-		this.I.client_settings.home.forEach(widget => {
-			try {
-				this.setWidget(widget);
-			} catch (e) {
-				// noop
-			}
-		});
+		this.home = this.$root.$data.os.i.client_settings.home;
 
-		if (!this.opts.customize) {
-			if (this.$refs.left.children.length == 0) {
-				this.$refs.left.parentNode.removeChild(this.$refs.left);
+		if (!this.customize) {
+			if ((this.$refs.left as Element).children.length == 0) {
+				(this.$refs.left as Element).parentNode.removeChild((this.$refs.left as Element));
 			}
-			if (this.$refs.right.children.length == 0) {
-				this.$refs.right.parentNode.removeChild(this.$refs.right);
+			if ((this.$refs.right as Element).children.length == 0) {
+				(this.$refs.right as Element).parentNode.removeChild((this.$refs.right as Element));
 			}
 		}
 
-		if (this.opts.customize) {
-			dialog('%fa:info-circle%カスタマイズのヒント',
+		if (this.customize) {
+			/*dialog('%fa:info-circle%カスタマイズのヒント',
 				'<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' +
 				'<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' +
 				'<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' +
 				'<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>',
 			[{
 				text: 'Got it!'
-			}]);
+			}]);*/
 
 			const sortableOption = {
 				group: 'kyoppie',
@@ -220,151 +214,151 @@ export default {
 					const el = evt.item;
 					const id = el.getAttribute('data-widget-id');
 					el.parentNode.removeChild(el);
-					this.I.client_settings.home = this.I.client_settings.home.filter(w => w.id != id);
+					this.$root.$data.os.i.client_settings.home = this.$root.$data.os.i.client_settings.home.filter(w => w.id != id);
 					this.saveHome();
 				}
 			}));
 		}
 	},
 	beforeDestroy() {
-		this.I.off('refreshed', this.onMeRefreshed);
+		this.$root.$data.os.i.off('refreshed', this.onMeRefreshed);
 
 		this.home.forEach(widget => {
 			widget.unmount();
 		});
 	}
-};
+});
 </script>
 
 <style lang="stylus" scoped>
-	:scope
-		display block
+.mk-home
+	display block
 
-		&[data-customize]
-			padding-top 48px
-			background-image url('/assets/desktop/grid.svg')
+	&[data-customize]
+		padding-top 48px
+		background-image url('/assets/desktop/grid.svg')
 
-			> .main > main > *:not(.maintop)
-				cursor not-allowed
+		> .main > main > *:not(.maintop)
+			cursor not-allowed
+
+			> *
+				pointer-events none
+
+	&:not([data-customize])
+		> .main > *:empty
+			display none
+
+	> .customize
+		position fixed
+		z-index 1000
+		top 0
+		left 0
+		width 100%
+		height 48px
+		background #f7f7f7
+		box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
+
+		> a
+			display block
+			position absolute
+			z-index 1001
+			top 0
+			right 0
+			padding 0 16px
+			line-height 48px
+			text-decoration none
+			color $theme-color-foreground
+			background $theme-color
+			transition background 0.1s ease
+
+			&:hover
+				background lighten($theme-color, 10%)
+
+			&:active
+				background darken($theme-color, 10%)
+				transition background 0s ease
+
+			> [data-fa]
+				margin-right 8px
+
+		> div
+			display flex
+			margin 0 auto
+			max-width 1200px - 32px
+
+			> div
+				width 50%
+
+				&.adder
+					> p
+						display inline
+						line-height 48px
+
+				&.trash
+					border-left solid 1px #ddd
+
+					> div
+						width 100%
+						height 100%
+
+					> p
+						position absolute
+						top 0
+						left 0
+						width 100%
+						line-height 48px
+						margin 0
+						text-align center
+						pointer-events none
+
+	> .main
+		display flex
+		justify-content center
+		margin 0 auto
+		max-width 1200px
+
+		> *
+			.customize-container
+				cursor move
 
 				> *
 					pointer-events none
 
-		&:not([data-customize])
-			> .main > *:empty
-				display none
+		> main
+			padding 16px
+			width calc(100% - 275px * 2)
 
-		> .customize
-			position fixed
-			z-index 1000
-			top 0
-			left 0
-			width 100%
-			height 48px
-			background #f7f7f7
-			box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
+			> *:not(.maintop):not(:last-child)
+			> .maintop > *:not(:last-child)
+				margin-bottom 16px
 
-			> a
-				display block
-				position absolute
-				z-index 1001
-				top 0
-				right 0
-				padding 0 16px
-				line-height 48px
-				text-decoration none
-				color $theme-color-foreground
-				background $theme-color
-				transition background 0.1s ease
+			> .maintop
+				min-height 64px
+				margin-bottom 16px
 
-				&:hover
-					background lighten($theme-color, 10%)
-
-				&:active
-					background darken($theme-color, 10%)
-					transition background 0s ease
-
-				> [data-fa]
-					margin-right 8px
-
-			> div
-				display flex
-				margin 0 auto
-				max-width 1200px - 32px
-
-				> div
-					width 50%
-
-					&.adder
-						> p
-							display inline
-							line-height 48px
-
-					&.trash
-						border-left solid 1px #ddd
-
-						> div
-							width 100%
-							height 100%
-
-						> p
-							position absolute
-							top 0
-							left 0
-							width 100%
-							line-height 48px
-							margin 0
-							text-align center
-							pointer-events none
-
-		> .main
-			display flex
-			justify-content center
-			margin 0 auto
-			max-width 1200px
+		> *:not(main)
+			width 275px
 
 			> *
-				.customize-container
-					cursor move
+				padding 16px 0 16px 0
 
-					> *
-						pointer-events none
+				> *:not(:last-child)
+					margin-bottom 16px
+
+		> .left
+			padding-left 16px
+
+		> .right
+			padding-right 16px
+
+		@media (max-width 1100px)
+			> *:not(main)
+				display none
 
 			> main
-				padding 16px
-				width calc(100% - 275px * 2)
-
-				> *:not(.maintop):not(:last-child)
-				> .maintop > *:not(:last-child)
-					margin-bottom 16px
-
-				> .maintop
-					min-height 64px
-					margin-bottom 16px
-
-			> *:not(main)
-				width 275px
-
-				> *
-					padding 16px 0 16px 0
-
-					> *:not(:last-child)
-						margin-bottom 16px
-
-			> .left
-				padding-left 16px
-
-			> .right
-				padding-right 16px
-
-			@media (max-width 1100px)
-				> *:not(main)
-					display none
-
-				> main
-					float none
-					width 100%
-					max-width 700px
-					margin 0 auto
+				float none
+				width 100%
+				max-width 700px
+				margin 0 auto
 
 </style>
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index f628dee88..8c490ef6d 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -1,5 +1,7 @@
 import Vue from 'vue';
 
 import ui from './ui.vue';
+import home from './home.vue';
 
 Vue.component('mk-ui', ui);
+Vue.component('mk-home', home);

From 7d6a38d455e7b4360ee42956166190d920c7ca8c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 13:09:46 +0900
Subject: [PATCH 035/286] wip

---
 src/web/app/desktop/views/components/home.vue | 20 +++++++++----------
 .../app/desktop/views/components/timeline.vue |  0
 src/web/app/desktop/views/pages/home.vue      |  2 +-
 3 files changed, 11 insertions(+), 11 deletions(-)
 create mode 100644 src/web/app/desktop/views/components/timeline.vue

diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 987f272a0..076cbabe8 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -41,10 +41,10 @@
 			<div ref="left" data-place="left">
 				<template v-for="widget in leftWidgets">
 					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
-						<component :is="widget.name" :widget="widget" :ref="widget.id"/>
+						<component :is="'mk-hw-' + widget.name" :widget="widget" :ref="widget.id"/>
 					</div>
 					<template v-else>
-						<component :is="widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
+						<component :is="'mk-hw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
 					</template>
 				</template>
 			</div>
@@ -53,24 +53,24 @@
 			<div class="maintop" ref="maintop" data-place="main" v-if="customize">
 				<template v-for="widget in centerWidgets">
 					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
-						<component :is="widget.name" :widget="widget" :ref="widget.id"/>
+						<component :is="'mk-hw-' + widget.name" :widget="widget" :ref="widget.id"/>
 					</div>
 					<template v-else>
-						<component :is="widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
+						<component :is="'mk-hw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
 					</template>
 				</template>
 			</div>
-			<mk-timeline-home-widget ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
-			<mk-mentions-home-widget ref="tl" @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
+			<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
+			<mk-mentions ref="tl" @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
 		</main>
 		<div class="right">
 			<div ref="right" data-place="right">
 				<template v-for="widget in rightWidgets">
 					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
-						<component :is="widget.name" :widget="widget" :ref="widget.id"/>
+						<component :is="'mk-hw-' + widget.name" :widget="widget" :ref="widget.id"/>
 					</div>
 					<template v-else>
-						<component :is="widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
+						<component :is="'mk-hw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
 					</template>
 				</template>
 			</div>
@@ -79,9 +79,9 @@
 </div>
 </template>
 
-<script lang="typescript">
+<script lang="ts">
 import Vue from 'vue';
-import uuid from 'uuid';
+import * as uuid from 'uuid';
 import Sortable from 'sortablejs';
 
 export default Vue.extend({
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/web/app/desktop/views/pages/home.vue b/src/web/app/desktop/views/pages/home.vue
index 8a380fad0..ff20291d5 100644
--- a/src/web/app/desktop/views/pages/home.vue
+++ b/src/web/app/desktop/views/pages/home.vue
@@ -1,6 +1,6 @@
 <template>
 	<mk-ui>
-		<home ref="home" :mode="mode"/>
+		<mk-home ref="home" :mode="mode"/>
 	</mk-ui>
 </template>
 

From b8f7eb9b586fe8d5c2debeb34b1fadc4c98a5c1e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 16:52:37 +0900
Subject: [PATCH 036/286] wip

---
 .../app/desktop/-tags/sub-post-content.tag    |  54 --
 src/web/app/desktop/-tags/timeline.tag        | 704 ------------------
 .../views/components/sub-post-content.vue     |  55 ++
 .../views/components/timeline-post-sub.vue    | 108 +++
 .../views/components/timeline-post.vue        | 515 +++++++++++++
 .../app/desktop/views/components/timeline.vue |  85 +++
 6 files changed, 763 insertions(+), 758 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/sub-post-content.tag
 delete mode 100644 src/web/app/desktop/-tags/timeline.tag
 create mode 100644 src/web/app/desktop/views/components/sub-post-content.vue
 create mode 100644 src/web/app/desktop/views/components/timeline-post-sub.vue
 create mode 100644 src/web/app/desktop/views/components/timeline-post.vue

diff --git a/src/web/app/desktop/-tags/sub-post-content.tag b/src/web/app/desktop/-tags/sub-post-content.tag
deleted file mode 100644
index 40b3b3005..000000000
--- a/src/web/app/desktop/-tags/sub-post-content.tag
+++ /dev/null
@@ -1,54 +0,0 @@
-<mk-sub-post-content>
-	<div class="body">
-		<a class="reply" v-if="post.reply_id">
-			%fa:reply%
-		</a>
-		<span ref="text"></span>
-		<a class="quote" v-if="post.repost_id" href={ '/post:' + post.repost_id }>RP: ...</a>
-	</div>
-	<details v-if="post.media">
-		<summary>({ post.media.length }つのメディア)</summary>
-		<mk-images images={ post.media }/>
-	</details>
-	<details v-if="post.poll">
-		<summary>投票</summary>
-		<mk-poll post={ post }/>
-	</details>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow-wrap break-word
-
-			> .body
-				> .reply
-					margin-right 6px
-					color #717171
-
-				> .quote
-					margin-left 4px
-					font-style oblique
-					color #a0bf46
-
-			mk-poll
-				font-size 80%
-
-	</style>
-	<script lang="typescript">
-		import compile from '../../common/scripts/text-compiler';
-
-		this.mixin('user-preview');
-
-		this.post = this.opts.post;
-
-		this.on('mount', () => {
-			if (this.post.text) {
-				const tokens = this.post.ast;
-				this.$refs.text.innerHTML = compile(tokens, false);
-
-				Array.from(this.$refs.text.children).forEach(e => {
-					if (e.tagName == 'MK-URL') riot.mount(e);
-				});
-			}
-		});
-	</script>
-</mk-sub-post-content>
diff --git a/src/web/app/desktop/-tags/timeline.tag b/src/web/app/desktop/-tags/timeline.tag
deleted file mode 100644
index 7f79d18b4..000000000
--- a/src/web/app/desktop/-tags/timeline.tag
+++ /dev/null
@@ -1,704 +0,0 @@
-<mk-timeline>
-	<template each={ post, i in posts }>
-		<mk-timeline-post post={ post }/>
-		<p class="date" v-if="i != posts.length - 1 && post._date != posts[i + 1]._date"><span>%fa:angle-up%{ post._datetext }</span><span>%fa:angle-down%{ posts[i + 1]._datetext }</span></p>
-	</template>
-	<footer data-yield="footer">
-		<yield from="footer"/>
-	</footer>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> .date
-				display block
-				margin 0
-				line-height 32px
-				font-size 14px
-				text-align center
-				color #aaa
-				background #fdfdfd
-				border-bottom solid 1px #eaeaea
-
-				span
-					margin 0 16px
-
-				[data-fa]
-					margin-right 8px
-
-			> footer
-				padding 16px
-				text-align center
-				color #ccc
-				border-top solid 1px #eaeaea
-				border-bottom-left-radius 4px
-				border-bottom-right-radius 4px
-
-	</style>
-	<script lang="typescript">
-		this.posts = [];
-
-		this.on('update', () => {
-			this.posts.forEach(post => {
-				const date = new Date(post.created_at).getDate();
-				const month = new Date(post.created_at).getMonth() + 1;
-				post._date = date;
-				post._datetext = `${month}月 ${date}日`;
-			});
-		});
-
-		this.setPosts = posts => {
-			this.update({
-				posts: posts
-			});
-		};
-
-		this.prependPosts = posts => {
-			posts.forEach(post => {
-				this.posts.push(post);
-				this.update();
-			});
-		}
-
-		this.addPost = post => {
-			this.posts.unshift(post);
-			this.update();
-		};
-
-		this.tail = () => {
-			return this.posts[this.posts.length - 1];
-		};
-
-		this.clear = () => {
-			this.posts = [];
-			this.update();
-		};
-
-		this.focus = () => {
-			this.root.children[0].focus();
-		};
-
-	</script>
-</mk-timeline>
-
-<mk-timeline-post tabindex="-1" title={ title } onkeydown={ onKeyDown } dblclick={ onDblClick }>
-	<div class="reply-to" v-if="p.reply">
-		<mk-timeline-post-sub post={ p.reply }/>
-	</div>
-	<div class="repost" v-if="isRepost">
-		<p>
-			<a class="avatar-anchor" href={ '/' + post.user.username } data-user-preview={ post.user_id }>
-				<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=32' } alt="avatar"/>
-			</a>
-			%fa:retweet%{'%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{'))}<a class="name" href={ '/' + post.user.username } data-user-preview={ post.user_id }>{ post.user.name }</a>{'%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1)}
-		</p>
-		<mk-time time={ post.created_at }/>
-	</div>
-	<article>
-		<a class="avatar-anchor" href={ '/' + p.user.username }>
-			<img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=64' } alt="avatar" data-user-preview={ p.user.id }/>
-		</a>
-		<div class="main">
-			<header>
-				<a class="name" href={ '/' + p.user.username } data-user-preview={ p.user.id }>{ p.user.name }</a>
-				<span class="is-bot" v-if="p.user.is_bot">bot</span>
-				<span class="username">@{ p.user.username }</span>
-				<div class="info">
-					<span class="app" v-if="p.app">via <b>{ p.app.name }</b></span>
-					<a class="created-at" href={ url }>
-						<mk-time time={ p.created_at }/>
-					</a>
-				</div>
-			</header>
-			<div class="body">
-				<div class="text" ref="text">
-					<p class="channel" v-if="p.channel != null"><a href={ _CH_URL_ + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
-					<a class="reply" v-if="p.reply">
-						%fa:reply%
-					</a>
-					<p class="dummy"></p>
-					<a class="quote" v-if="p.repost != null">RP:</a>
-				</div>
-				<div class="media" v-if="p.media">
-					<mk-images images={ p.media }/>
-				</div>
-				<mk-poll v-if="p.poll" post={ p } ref="pollViewer"/>
-				<div class="repost" v-if="p.repost">%fa:quote-right -flip-h%
-					<mk-post-preview class="repost" post={ p.repost }/>
-				</div>
-			</div>
-			<footer>
-				<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
-				<button @click="reply" title="%i18n:desktop.tags.mk-timeline-post.reply%">
-					%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
-				</button>
-				<button @click="repost" title="%i18n:desktop.tags.mk-timeline-post.repost%">
-					%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
-				</button>
-				<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-post.add-reaction%">
-					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
-				</button>
-				<button @click="menu" ref="menuButton">
-					%fa:ellipsis-h%
-				</button>
-				<button @click="toggleDetail" title="%i18n:desktop.tags.mk-timeline-post.detail">
-					<template v-if="!isDetailOpened">%fa:caret-down%</template>
-					<template v-if="isDetailOpened">%fa:caret-up%</template>
-				</button>
-			</footer>
-		</div>
-	</article>
-	<div class="detail" v-if="isDetailOpened">
-		<mk-post-status-graph width="462" height="130" post={ p }/>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 0
-			background #fff
-			border-bottom solid 1px #eaeaea
-
-			&:first-child
-				border-top-left-radius 6px
-				border-top-right-radius 6px
-
-				> .repost
-					border-top-left-radius 6px
-					border-top-right-radius 6px
-
-			&:last-of-type
-				border-bottom none
-
-			&:focus
-				z-index 1
-
-				&:after
-					content ""
-					pointer-events none
-					position absolute
-					top 2px
-					right 2px
-					bottom 2px
-					left 2px
-					border 2px solid rgba($theme-color, 0.3)
-					border-radius 4px
-
-			> .repost
-				color #9dbb00
-				background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
-
-				> p
-					margin 0
-					padding 16px 32px
-					line-height 28px
-
-					.avatar-anchor
-						display inline-block
-
-						.avatar
-							vertical-align bottom
-							width 28px
-							height 28px
-							margin 0 8px 0 0
-							border-radius 6px
-
-					[data-fa]
-						margin-right 4px
-
-					.name
-						font-weight bold
-
-				> mk-time
-					position absolute
-					top 16px
-					right 32px
-					font-size 0.9em
-					line-height 28px
-
-				& + article
-					padding-top 8px
-
-			> .reply-to
-				padding 0 16px
-				background rgba(0, 0, 0, 0.0125)
-
-				> mk-post-preview
-					background transparent
-
-			> article
-				padding 28px 32px 18px 32px
-
-				&:after
-					content ""
-					display block
-					clear both
-
-				&:hover
-					> .main > footer > button
-						color #888
-
-				> .avatar-anchor
-					display block
-					float left
-					margin 0 16px 10px 0
-					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)
-
-					> header
-						display flex
-						margin-bottom 4px
-						white-space nowrap
-						line-height 1.4
-
-						> .name
-							display block
-							margin 0 .5em 0 0
-							padding 0
-							overflow hidden
-							color #777
-							font-size 1em
-							font-weight 700
-							text-align left
-							text-decoration none
-							text-overflow ellipsis
-
-							&:hover
-								text-decoration underline
-
-						> .is-bot
-							text-align left
-							margin 0 .5em 0 0
-							padding 1px 6px
-							font-size 12px
-							color #aaa
-							border solid 1px #ddd
-							border-radius 3px
-
-						> .username
-							text-align left
-							margin 0 .5em 0 0
-							color #ccc
-
-						> .info
-							margin-left auto
-							text-align right
-							font-size 0.9em
-
-							> .app
-								margin-right 8px
-								padding-right 8px
-								color #ccc
-								border-right solid 1px #eaeaea
-
-							> .created-at
-								color #c0c0c0
-
-					> .body
-
-						> .text
-							cursor default
-							display block
-							margin 0
-							padding 0
-							overflow-wrap break-word
-							font-size 1.1em
-							color #717171
-
-							> .dummy
-								display none
-
-							mk-url-preview
-								margin-top 8px
-
-							> .channel
-								margin 0
-
-							> .reply
-								margin-right 8px
-								color #717171
-
-							> .quote
-								margin-left 4px
-								font-style oblique
-								color #a0bf46
-
-							code
-								padding 4px 8px
-								margin 0 0.5em
-								font-size 80%
-								color #525252
-								background #f8f8f8
-								border-radius 2px
-
-							pre > code
-								padding 16px
-								margin 0
-
-							[data-is-me]:after
-								content "you"
-								padding 0 4px
-								margin-left 4px
-								font-size 80%
-								color $theme-color-foreground
-								background $theme-color
-								border-radius 4px
-
-						> mk-poll
-							font-size 80%
-
-						> .repost
-							margin 8px 0
-
-							> [data-fa]:first-child
-								position absolute
-								top -8px
-								left -8px
-								z-index 1
-								color #c0dac6
-								font-size 28px
-								background #fff
-
-							> mk-post-preview
-								padding 16px
-								border dashed 1px #c0dac6
-								border-radius 8px
-
-					> footer
-						> button
-							margin 0 28px 0 0
-							padding 0 8px
-							line-height 32px
-							font-size 1em
-							color #ddd
-							background transparent
-							border none
-							cursor pointer
-
-							&:hover
-								color #666
-
-							> .count
-								display inline
-								margin 0 0 0 8px
-								color #999
-
-							&.reacted
-								color $theme-color
-
-							&:last-child
-								position absolute
-								right 0
-								margin 0
-
-			> .detail
-				padding-top 4px
-				background rgba(0, 0, 0, 0.0125)
-
-	</style>
-	<script lang="typescript">
-		import compile from '../../common/scripts/text-compiler';
-		import dateStringify from '../../common/scripts/date-stringify';
-
-		this.mixin('i');
-		this.mixin('api');
-		this.mixin('user-preview');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.isDetailOpened = false;
-
-		this.set = post => {
-			this.post = post;
-			this.isRepost = this.post.repost && this.post.text == null && this.post.media_ids == null && this.post.poll == null;
-			this.p = this.isRepost ? this.post.repost : this.post;
-			this.p.reactions_count = this.p.reaction_counts ? Object.keys(this.p.reaction_counts).map(key => this.p.reaction_counts[key]).reduce((a, b) => a + b) : 0;
-			this.title = dateStringify(this.p.created_at);
-			this.url = `/${this.p.user.username}/${this.p.id}`;
-		};
-
-		this.set(this.opts.post);
-
-		this.refresh = post => {
-			this.set(post);
-			this.update();
-			if (this.$refs.reactionsViewer) this.$refs.reactionsViewer.update({
-				post
-			});
-			if (this.$refs.pollViewer) this.$refs.pollViewer.init(post);
-		};
-
-		this.onStreamPostUpdated = data => {
-			const post = data.post;
-			if (post.id == this.post.id) {
-				this.refresh(post);
-			}
-		};
-
-		this.onStreamConnected = () => {
-			this.capture();
-		};
-
-		this.capture = withHandler => {
-			if (this.SIGNIN) {
-				this.connection.send({
-					type: 'capture',
-					id: this.post.id
-				});
-				if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated);
-			}
-		};
-
-		this.decapture = withHandler => {
-			if (this.SIGNIN) {
-				this.connection.send({
-					type: 'decapture',
-					id: this.post.id
-				});
-				if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated);
-			}
-		};
-
-		this.on('mount', () => {
-			this.capture(true);
-
-			if (this.SIGNIN) {
-				this.connection.on('_connected_', this.onStreamConnected);
-			}
-
-			if (this.p.text) {
-				const tokens = this.p.ast;
-
-				this.$refs.text.innerHTML = this.$refs.text.innerHTML.replace('<p class="dummy"></p>', compile(tokens));
-
-				Array.from(this.$refs.text.children).forEach(e => {
-					if (e.tagName == 'MK-URL') riot.mount(e);
-				});
-
-				// URLをプレビュー
-				tokens
-				.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
-				.map(t => {
-					riot.mount(this.$refs.text.appendChild(document.createElement('mk-url-preview')), {
-						url: t.url
-					});
-				});
-			}
-		});
-
-		this.on('unmount', () => {
-			this.decapture(true);
-			this.connection.off('_connected_', this.onStreamConnected);
-			this.stream.dispose(this.connectionId);
-		});
-
-		this.reply = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-post-form-window')), {
-				reply: this.p
-			});
-		};
-
-		this.repost = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-repost-form-window')), {
-				post: this.p
-			});
-		};
-
-		this.react = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), {
-				source: this.$refs.reactButton,
-				post: this.p
-			});
-		};
-
-		this.menu = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
-				source: this.$refs.menuButton,
-				post: this.p
-			});
-		};
-
-		this.toggleDetail = () => {
-			this.update({
-				isDetailOpened: !this.isDetailOpened
-			});
-		};
-
-		this.onKeyDown = e => {
-			let shouldBeCancel = true;
-
-			switch (true) {
-				case e.which == 38: // [↑]
-				case e.which == 74: // [j]
-				case e.which == 9 && e.shiftKey: // [Shift] + [Tab]
-					focus(this.root, e => e.previousElementSibling);
-					break;
-
-				case e.which == 40: // [↓]
-				case e.which == 75: // [k]
-				case e.which == 9: // [Tab]
-					focus(this.root, e => e.nextElementSibling);
-					break;
-
-				case e.which == 81: // [q]
-				case e.which == 69: // [e]
-					this.repost();
-					break;
-
-				case e.which == 70: // [f]
-				case e.which == 76: // [l]
-					this.like();
-					break;
-
-				case e.which == 82: // [r]
-					this.reply();
-					break;
-
-				default:
-					shouldBeCancel = false;
-			}
-
-			if (shouldBeCancel) e.preventDefault();
-		};
-
-		this.onDblClick = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-detailed-post-window')), {
-				post: this.p.id
-			});
-		};
-
-		function focus(el, fn) {
-			const target = fn(el);
-			if (target) {
-				if (target.hasAttribute('tabindex')) {
-					target.focus();
-				} else {
-					focus(target, fn);
-				}
-			}
-		}
-	</script>
-</mk-timeline-post>
-
-<mk-timeline-post-sub title={ title }>
-	<article>
-		<a class="avatar-anchor" href={ '/' + post.user.username }>
-			<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar" data-user-preview={ post.user_id }/>
-		</a>
-		<div class="main">
-			<header>
-				<a class="name" href={ '/' + post.user.username } data-user-preview={ post.user_id }>{ post.user.name }</a>
-				<span class="username">@{ post.user.username }</span>
-				<a class="created-at" href={ '/' + post.user.username + '/' + post.id }>
-					<mk-time time={ post.created_at }/>
-				</a>
-			</header>
-			<div class="body">
-				<mk-sub-post-content class="text" post={ post }/>
-			</div>
-		</div>
-	</article>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 0
-			font-size 0.9em
-
-			> article
-				padding 16px
-
-				&:after
-					content ""
-					display block
-					clear both
-
-				&:hover
-					> .main > footer > button
-						color #888
-
-				> .avatar-anchor
-					display block
-					float left
-					margin 0 14px 0 0
-
-					> .avatar
-						display block
-						width 52px
-						height 52px
-						margin 0
-						border-radius 8px
-						vertical-align bottom
-
-				> .main
-					float left
-					width calc(100% - 66px)
-
-					> header
-						display flex
-						margin-bottom 2px
-						white-space nowrap
-						line-height 21px
-
-						> .name
-							display block
-							margin 0 .5em 0 0
-							padding 0
-							overflow hidden
-							color #607073
-							font-size 1em
-							font-weight 700
-							text-align left
-							text-decoration none
-							text-overflow ellipsis
-
-							&:hover
-								text-decoration underline
-
-						> .username
-							text-align left
-							margin 0 .5em 0 0
-							color #d1d8da
-
-						> .created-at
-							margin-left auto
-							color #b2b8bb
-
-					> .body
-
-						> .text
-							cursor default
-							margin 0
-							padding 0
-							font-size 1.1em
-							color #717171
-
-							pre
-								max-height 120px
-								font-size 80%
-
-	</style>
-	<script lang="typescript">
-		import dateStringify from '../../common/scripts/date-stringify';
-
-		this.mixin('user-preview');
-
-		this.post = this.opts.post;
-		this.title = dateStringify(this.post.created_at);
-	</script>
-</mk-timeline-post-sub>
diff --git a/src/web/app/desktop/views/components/sub-post-content.vue b/src/web/app/desktop/views/components/sub-post-content.vue
new file mode 100644
index 000000000..2463e8a9b
--- /dev/null
+++ b/src/web/app/desktop/views/components/sub-post-content.vue
@@ -0,0 +1,55 @@
+<template>
+<div class="mk-sub-post-content">
+	<div class="body">
+		<a class="reply" v-if="post.reply_id">%fa:reply%</a>
+		<span ref="text"></span>
+		<a class="quote" v-if="post.repost_id" :href="`/post:${post.repost_id}`">RP: ...</a>
+	</div>
+	<details v-if="post.media">
+		<summary>({{ post.media.length }}つのメディア)</summary>
+		<mk-images :images="post.media"/>
+	</details>
+	<details v-if="post.poll">
+		<summary>投票</summary>
+		<mk-poll :post="post"/>
+	</details>
+</div>
+</template>
+
+<script lang="typescript">
+	import compile from '../../common/scripts/text-compiler';
+
+	this.mixin('user-preview');
+
+	this.post = this.opts.post;
+
+	this.on('mount', () => {
+		if (this.post.text) {
+			const tokens = this.post.ast;
+			this.$refs.text.innerHTML = compile(tokens, false);
+
+			Array.from(this.$refs.text.children).forEach(e => {
+				if (e.tagName == 'MK-URL') riot.mount(e);
+			});
+		}
+	});
+</script>
+
+<style lang="stylus" scoped>
+.mk-sub-post-content
+	overflow-wrap break-word
+
+	> .body
+		> .reply
+			margin-right 6px
+			color #717171
+
+		> .quote
+			margin-left 4px
+			font-style oblique
+			color #a0bf46
+
+	mk-poll
+		font-size 80%
+
+</style>
diff --git a/src/web/app/desktop/views/components/timeline-post-sub.vue b/src/web/app/desktop/views/components/timeline-post-sub.vue
new file mode 100644
index 000000000..27820901f
--- /dev/null
+++ b/src/web/app/desktop/views/components/timeline-post-sub.vue
@@ -0,0 +1,108 @@
+<template>
+<div class="mk-timeline-post-sub" :title="title">
+	<a class="avatar-anchor" :href="`/${post.user.username}`">
+		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" :v-user-preview="post.user_id"/>
+	</a>
+	<div class="main">
+		<header>
+			<a class="name" :href="`/${post.user.username}`" :v-user-preview="post.user_id">{{ post.user.name }}</a>
+			<span class="username">@{{ post.user.username }}</span>
+			<a class="created-at" :href="`/${post.user.username}/${post.id}`">
+				<mk-time :time="post.created_at"/>
+			</a>
+		</header>
+		<div class="body">
+			<mk-sub-post-content class="text" :post="post"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="typescript">
+	import dateStringify from '../../common/scripts/date-stringify';
+
+	this.mixin('user-preview');
+
+	this.post = this.opts.post;
+	this.title = dateStringify(this.post.created_at);
+</script>
+
+<style lang="stylus" scoped>
+.mk-timeline-post-sub
+	margin 0
+	padding 0
+	font-size 0.9em
+
+	> article
+		padding 16px
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		&:hover
+			> .main > footer > button
+				color #888
+
+		> .avatar-anchor
+			display block
+			float left
+			margin 0 14px 0 0
+
+			> .avatar
+				display block
+				width 52px
+				height 52px
+				margin 0
+				border-radius 8px
+				vertical-align bottom
+
+		> .main
+			float left
+			width calc(100% - 66px)
+
+			> header
+				display flex
+				margin-bottom 2px
+				white-space nowrap
+				line-height 21px
+
+				> .name
+					display block
+					margin 0 .5em 0 0
+					padding 0
+					overflow hidden
+					color #607073
+					font-size 1em
+					font-weight 700
+					text-align left
+					text-decoration none
+					text-overflow ellipsis
+
+					&:hover
+						text-decoration underline
+
+				> .username
+					text-align left
+					margin 0 .5em 0 0
+					color #d1d8da
+
+				> .created-at
+					margin-left auto
+					color #b2b8bb
+
+			> .body
+
+				> .text
+					cursor default
+					margin 0
+					padding 0
+					font-size 1.1em
+					color #717171
+
+					pre
+						max-height 120px
+						font-size 80%
+
+</style>
diff --git a/src/web/app/desktop/views/components/timeline-post.vue b/src/web/app/desktop/views/components/timeline-post.vue
new file mode 100644
index 000000000..a50d0c7bd
--- /dev/null
+++ b/src/web/app/desktop/views/components/timeline-post.vue
@@ -0,0 +1,515 @@
+<template>
+<div class="mk-timeline-post" tabindex="-1" :title="title" @keydown="onKeyDown" @dblclick="onDblClick">
+	<div class="reply-to" v-if="p.reply">
+		<mk-timeline-post-sub post="p.reply"/>
+	</div>
+	<div class="repost" v-if="isRepost">
+		<p>
+			<a class="avatar-anchor" :href="`/${post.user.username}`" :v-user-preview="post.user_id">
+				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
+			</a>
+			%fa:retweet%{{'%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{'))}}<a class="name" :href="`/${post.user.username}`" :v-user-preview="post.user_id">{{ post.user.name }}</a>{{'%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1)}}
+		</p>
+		<mk-time :time="post.created_at"/>
+	</div>
+	<article>
+		<a class="avatar-anchor" :href="`/${p.user.username}`">
+			<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=64`" alt="avatar" :v-user-preview="p.user.id"/>
+		</a>
+		<div class="main">
+			<header>
+				<a class="name" :href="`/${p.user.username}`" :v-user-preview="p.user.id">{{ p.user.name }}</a>
+				<span class="is-bot" v-if="p.user.is_bot">bot</span>
+				<span class="username">@{{ p.user.username }}</span>
+				<div class="info">
+					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
+					<a class="created-at" :href="url">
+						<mk-time time="p.created_at"/>
+					</a>
+				</div>
+			</header>
+			<div class="body">
+				<div class="text" ref="text">
+					<p class="channel" v-if="p.channel"><a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:</p>
+					<a class="reply" v-if="p.reply">%fa:reply%</a>
+					<p class="dummy"></p>
+					<a class="quote" v-if="p.repost">RP:</a>
+				</div>
+				<div class="media" v-if="p.media">
+					<mk-images :images="p.media"/>
+				</div>
+				<mk-poll v-if="p.poll" :post="p" ref="pollViewer"/>
+				<div class="repost" v-if="p.repost">%fa:quote-right -flip-h%
+					<mk-post-preview class="repost" :post="p.repost"/>
+				</div>
+			</div>
+			<footer>
+				<mk-reactions-viewer :post="p" ref="reactionsViewer"/>
+				<button @click="reply" title="%i18n:desktop.tags.mk-timeline-post.reply%">
+					%fa:reply%<p class="count" v-if="p.replies_count > 0">{{ p.replies_count }}</p>
+				</button>
+				<button @click="repost" title="%i18n:desktop.tags.mk-timeline-post.repost%">
+					%fa:retweet%<p class="count" v-if="p.repost_count > 0">{{ p.repost_count }}</p>
+				</button>
+				<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-post.add-reaction%">
+					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
+				</button>
+				<button @click="menu" ref="menuButton">
+					%fa:ellipsis-h%
+				</button>
+				<button @click="toggleDetail" title="%i18n:desktop.tags.mk-timeline-post.detail">
+					<template v-if="!isDetailOpened">%fa:caret-down%</template>
+					<template v-if="isDetailOpened">%fa:caret-up%</template>
+				</button>
+			</footer>
+		</div>
+	</article>
+	<div class="detail" v-if="isDetailOpened">
+		<mk-post-status-graph width="462" height="130" :post="p"/>
+	</div>
+</div>
+</template>
+
+<script lang="typescript">
+import compile from '../../common/scripts/text-compiler';
+import dateStringify from '../../common/scripts/date-stringify';
+
+this.mixin('i');
+this.mixin('api');
+this.mixin('user-preview');
+
+this.mixin('stream');
+this.connection = this.stream.getConnection();
+this.connectionId = this.stream.use();
+
+this.isDetailOpened = false;
+
+this.set = post => {
+	this.post = post;
+	this.isRepost = this.post.repost && this.post.text == null && this.post.media_ids == null && this.post.poll == null;
+	this.p = this.isRepost ? this.post.repost : this.post;
+	this.p.reactions_count = this.p.reaction_counts ? Object.keys(this.p.reaction_counts).map(key => this.p.reaction_counts[key]).reduce((a, b) => a + b) : 0;
+	this.title = dateStringify(this.p.created_at);
+	this.url = `/${this.p.user.username}/${this.p.id}`;
+};
+
+this.set(this.opts.post);
+
+this.refresh = post => {
+	this.set(post);
+	this.update();
+	if (this.$refs.reactionsViewer) this.$refs.reactionsViewer.update({
+		post
+	});
+	if (this.$refs.pollViewer) this.$refs.pollViewer.init(post);
+};
+
+this.onStreamPostUpdated = data => {
+	const post = data.post;
+	if (post.id == this.post.id) {
+		this.refresh(post);
+	}
+};
+
+this.onStreamConnected = () => {
+	this.capture();
+};
+
+this.capture = withHandler => {
+	if (this.SIGNIN) {
+		this.connection.send({
+			type: 'capture',
+			id: this.post.id
+		});
+		if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated);
+	}
+};
+
+this.decapture = withHandler => {
+	if (this.SIGNIN) {
+		this.connection.send({
+			type: 'decapture',
+			id: this.post.id
+		});
+		if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated);
+	}
+};
+
+this.on('mount', () => {
+	this.capture(true);
+
+	if (this.SIGNIN) {
+		this.connection.on('_connected_', this.onStreamConnected);
+	}
+
+	if (this.p.text) {
+		const tokens = this.p.ast;
+
+		this.$refs.text.innerHTML = this.$refs.text.innerHTML.replace('<p class="dummy"></p>', compile(tokens));
+
+		Array.from(this.$refs.text.children).forEach(e => {
+			if (e.tagName == 'MK-URL') riot.mount(e);
+		});
+
+		// URLをプレビュー
+		tokens
+		.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+		.map(t => {
+			riot.mount(this.$refs.text.appendChild(document.createElement('mk-url-preview')), {
+				url: t.url
+			});
+		});
+	}
+});
+
+this.on('unmount', () => {
+	this.decapture(true);
+	this.connection.off('_connected_', this.onStreamConnected);
+	this.stream.dispose(this.connectionId);
+});
+
+this.reply = () => {
+	riot.mount(document.body.appendChild(document.createElement('mk-post-form-window')), {
+		reply: this.p
+	});
+};
+
+this.repost = () => {
+	riot.mount(document.body.appendChild(document.createElement('mk-repost-form-window')), {
+		post: this.p
+	});
+};
+
+this.react = () => {
+	riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), {
+		source: this.$refs.reactButton,
+		post: this.p
+	});
+};
+
+this.menu = () => {
+	riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
+		source: this.$refs.menuButton,
+		post: this.p
+	});
+};
+
+this.toggleDetail = () => {
+	this.update({
+		isDetailOpened: !this.isDetailOpened
+	});
+};
+
+this.onKeyDown = e => {
+	let shouldBeCancel = true;
+
+	switch (true) {
+		case e.which == 38: // [↑]
+		case e.which == 74: // [j]
+		case e.which == 9 && e.shiftKey: // [Shift] + [Tab]
+			focus(this.root, e => e.previousElementSibling);
+			break;
+
+		case e.which == 40: // [↓]
+		case e.which == 75: // [k]
+		case e.which == 9: // [Tab]
+			focus(this.root, e => e.nextElementSibling);
+			break;
+
+		case e.which == 81: // [q]
+		case e.which == 69: // [e]
+			this.repost();
+			break;
+
+		case e.which == 70: // [f]
+		case e.which == 76: // [l]
+			this.like();
+			break;
+
+		case e.which == 82: // [r]
+			this.reply();
+			break;
+
+		default:
+			shouldBeCancel = false;
+	}
+
+	if (shouldBeCancel) e.preventDefault();
+};
+
+this.onDblClick = () => {
+	riot.mount(document.body.appendChild(document.createElement('mk-detailed-post-window')), {
+		post: this.p.id
+	});
+};
+
+function focus(el, fn) {
+	const target = fn(el);
+	if (target) {
+		if (target.hasAttribute('tabindex')) {
+			target.focus();
+		} else {
+			focus(target, fn);
+		}
+	}
+}
+</script>
+
+<style lang="stylus" scoped>
+.mk-timeline-post
+	margin 0
+	padding 0
+	background #fff
+	border-bottom solid 1px #eaeaea
+
+	&:first-child
+		border-top-left-radius 6px
+		border-top-right-radius 6px
+
+		> .repost
+			border-top-left-radius 6px
+			border-top-right-radius 6px
+
+	&:last-of-type
+		border-bottom none
+
+	&:focus
+		z-index 1
+
+		&:after
+			content ""
+			pointer-events none
+			position absolute
+			top 2px
+			right 2px
+			bottom 2px
+			left 2px
+			border 2px solid rgba($theme-color, 0.3)
+			border-radius 4px
+
+	> .repost
+		color #9dbb00
+		background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+
+		> p
+			margin 0
+			padding 16px 32px
+			line-height 28px
+
+			.avatar-anchor
+				display inline-block
+
+				.avatar
+					vertical-align bottom
+					width 28px
+					height 28px
+					margin 0 8px 0 0
+					border-radius 6px
+
+			[data-fa]
+				margin-right 4px
+
+			.name
+				font-weight bold
+
+		> mk-time
+			position absolute
+			top 16px
+			right 32px
+			font-size 0.9em
+			line-height 28px
+
+		& + article
+			padding-top 8px
+
+	> .reply-to
+		padding 0 16px
+		background rgba(0, 0, 0, 0.0125)
+
+		> mk-post-preview
+			background transparent
+
+	> article
+		padding 28px 32px 18px 32px
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		&:hover
+			> .main > footer > button
+				color #888
+
+		> .avatar-anchor
+			display block
+			float left
+			margin 0 16px 10px 0
+			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)
+
+			> header
+				display flex
+				margin-bottom 4px
+				white-space nowrap
+				line-height 1.4
+
+				> .name
+					display block
+					margin 0 .5em 0 0
+					padding 0
+					overflow hidden
+					color #777
+					font-size 1em
+					font-weight 700
+					text-align left
+					text-decoration none
+					text-overflow ellipsis
+
+					&:hover
+						text-decoration underline
+
+				> .is-bot
+					text-align left
+					margin 0 .5em 0 0
+					padding 1px 6px
+					font-size 12px
+					color #aaa
+					border solid 1px #ddd
+					border-radius 3px
+
+				> .username
+					text-align left
+					margin 0 .5em 0 0
+					color #ccc
+
+				> .info
+					margin-left auto
+					text-align right
+					font-size 0.9em
+
+					> .app
+						margin-right 8px
+						padding-right 8px
+						color #ccc
+						border-right solid 1px #eaeaea
+
+					> .created-at
+						color #c0c0c0
+
+			> .body
+
+				> .text
+					cursor default
+					display block
+					margin 0
+					padding 0
+					overflow-wrap break-word
+					font-size 1.1em
+					color #717171
+
+					> .dummy
+						display none
+
+					mk-url-preview
+						margin-top 8px
+
+					> .channel
+						margin 0
+
+					> .reply
+						margin-right 8px
+						color #717171
+
+					> .quote
+						margin-left 4px
+						font-style oblique
+						color #a0bf46
+
+					code
+						padding 4px 8px
+						margin 0 0.5em
+						font-size 80%
+						color #525252
+						background #f8f8f8
+						border-radius 2px
+
+					pre > code
+						padding 16px
+						margin 0
+
+					[data-is-me]:after
+						content "you"
+						padding 0 4px
+						margin-left 4px
+						font-size 80%
+						color $theme-color-foreground
+						background $theme-color
+						border-radius 4px
+
+				> mk-poll
+					font-size 80%
+
+				> .repost
+					margin 8px 0
+
+					> [data-fa]:first-child
+						position absolute
+						top -8px
+						left -8px
+						z-index 1
+						color #c0dac6
+						font-size 28px
+						background #fff
+
+					> mk-post-preview
+						padding 16px
+						border dashed 1px #c0dac6
+						border-radius 8px
+
+			> footer
+				> button
+					margin 0 28px 0 0
+					padding 0 8px
+					line-height 32px
+					font-size 1em
+					color #ddd
+					background transparent
+					border none
+					cursor pointer
+
+					&:hover
+						color #666
+
+					> .count
+						display inline
+						margin 0 0 0 8px
+						color #999
+
+					&.reacted
+						color $theme-color
+
+					&:last-child
+						position absolute
+						right 0
+						margin 0
+
+	> .detail
+		padding-top 4px
+		background rgba(0, 0, 0, 0.0125)
+
+</style>
+
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index e69de29bb..1431166a4 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -0,0 +1,85 @@
+<template>
+<div class="mk-timeline">
+	<template each={ post, i in posts }>
+		<mk-timeline-post post={ post }/>
+		<p class="date" v-if="i != posts.length - 1 && post._date != posts[i + 1]._date"><span>%fa:angle-up%{ post._datetext }</span><span>%fa:angle-down%{ posts[i + 1]._datetext }</span></p>
+	</template>
+	<footer data-yield="footer">
+		<yield from="footer"/>
+	</footer>
+</div>	
+</template>
+
+<script lang="typescript">
+this.posts = [];
+
+this.on('update', () => {
+	this.posts.forEach(post => {
+		const date = new Date(post.created_at).getDate();
+		const month = new Date(post.created_at).getMonth() + 1;
+		post._date = date;
+		post._datetext = `${month}月 ${date}日`;
+	});
+});
+
+this.setPosts = posts => {
+	this.update({
+		posts: posts
+	});
+};
+
+this.prependPosts = posts => {
+	posts.forEach(post => {
+		this.posts.push(post);
+		this.update();
+	});
+}
+
+this.addPost = post => {
+	this.posts.unshift(post);
+	this.update();
+};
+
+this.tail = () => {
+	return this.posts[this.posts.length - 1];
+};
+
+this.clear = () => {
+	this.posts = [];
+	this.update();
+};
+
+this.focus = () => {
+	this.root.children[0].focus();
+};
+
+</script>
+
+<style lang="stylus" scoped>
+.mk-timeline
+
+	> .date
+		display block
+		margin 0
+		line-height 32px
+		font-size 14px
+		text-align center
+		color #aaa
+		background #fdfdfd
+		border-bottom solid 1px #eaeaea
+
+		span
+			margin 0 16px
+
+		[data-fa]
+			margin-right 8px
+
+	> footer
+		padding 16px
+		text-align center
+		color #ccc
+		border-top solid 1px #eaeaea
+		border-bottom-left-radius 4px
+		border-bottom-right-radius 4px
+
+</style>

From b427807bf62504d1f249bf36662f7fd3ba87498b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 17:04:03 +0900
Subject: [PATCH 037/286] wip

---
 .../app/desktop/views/components/timeline.vue | 66 +++++++------------
 1 file changed, 24 insertions(+), 42 deletions(-)

diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index 1431166a4..c9cb7c8f8 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-timeline">
+<div class="mk-timeline" ref="root">
 	<template each={ post, i in posts }>
 		<mk-timeline-post post={ post }/>
 		<p class="date" v-if="i != posts.length - 1 && post._date != posts[i + 1]._date"><span>%fa:angle-up%{ post._datetext }</span><span>%fa:angle-down%{ posts[i + 1]._datetext }</span></p>
@@ -10,49 +10,31 @@
 </div>	
 </template>
 
-<script lang="typescript">
-this.posts = [];
+<script lang="ts">
+import Vue from 'vue';
 
-this.on('update', () => {
-	this.posts.forEach(post => {
-		const date = new Date(post.created_at).getDate();
-		const month = new Date(post.created_at).getMonth() + 1;
-		post._date = date;
-		post._datetext = `${month}月 ${date}日`;
-	});
+export default Vue.extend({
+	props: ['posts'],
+	computed: {
+		_posts(): any {
+			return this.posts.map(post => {
+				const date = new Date(post.created_at).getDate();
+				const month = new Date(post.created_at).getMonth() + 1;
+				post._date = date;
+				post._datetext = `${month}月 ${date}日`;
+				return post;
+			});
+		},
+		tail(): any {
+			return this.posts[this.posts.length - 1];
+		}
+	},
+	methods: {
+		focus() {
+			this.$refs.root.children[0].focus();
+		}
+	}
 });
-
-this.setPosts = posts => {
-	this.update({
-		posts: posts
-	});
-};
-
-this.prependPosts = posts => {
-	posts.forEach(post => {
-		this.posts.push(post);
-		this.update();
-	});
-}
-
-this.addPost = post => {
-	this.posts.unshift(post);
-	this.update();
-};
-
-this.tail = () => {
-	return this.posts[this.posts.length - 1];
-};
-
-this.clear = () => {
-	this.posts = [];
-	this.update();
-};
-
-this.focus = () => {
-	this.root.children[0].focus();
-};
-
 </script>
 
 <style lang="stylus" scoped>

From 202ac80fa8d0741d2b01e45cc0c926f39fbd6690 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 17:11:30 +0900
Subject: [PATCH 038/286] wip

---
 src/web/app/desktop/views/components/timeline.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index c9cb7c8f8..0e8b19f16 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="mk-timeline" ref="root">
-	<template each={ post, i in posts }>
-		<mk-timeline-post post={ post }/>
-		<p class="date" v-if="i != posts.length - 1 && post._date != posts[i + 1]._date"><span>%fa:angle-up%{ post._datetext }</span><span>%fa:angle-down%{ posts[i + 1]._datetext }</span></p>
+	<template v-for="(post, i) in _posts">
+		<mk-timeline-post :post="post" :key="post.id"/>
+		<p class="date" :key="post.id + '-time'" v-if="i != _posts.length - 1 && _post._date != _posts[i + 1]._date"><span>%fa:angle-up%{{ post._datetext }}</span><span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span></p>
 	</template>
 	<footer data-yield="footer">
 		<yield from="footer"/>

From 4f2041c1d9ce908bfaa673b9309e7834776eecf3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 17:43:00 +0900
Subject: [PATCH 039/286] wip

---
 .../views/components/timeline-post.vue        | 189 +++++++++---------
 .../app/desktop/views/components/timeline.vue |   2 +-
 2 files changed, 99 insertions(+), 92 deletions(-)

diff --git a/src/web/app/desktop/views/components/timeline-post.vue b/src/web/app/desktop/views/components/timeline-post.vue
index a50d0c7bd..50c8ecf99 100644
--- a/src/web/app/desktop/views/components/timeline-post.vue
+++ b/src/web/app/desktop/views/components/timeline-post.vue
@@ -70,104 +70,111 @@
 </div>
 </template>
 
-<script lang="typescript">
+<script lang="ts">
+import Vue from 'vue';
 import compile from '../../common/scripts/text-compiler';
 import dateStringify from '../../common/scripts/date-stringify';
 
-this.mixin('i');
-this.mixin('api');
-this.mixin('user-preview');
+export default Vue.extend({
+	props: ['post'],
+	data() {
+		return {
+			connection: null,
+			connectionId: null
+		};
+	},
+	computed: {
+		isRepost(): boolean {
+			return (this.post.repost &&
+				this.post.text == null &&
+				this.post.media_ids == null &&
+				this.post.poll == null);
+		},
+		p(): any {
+			return this.isRepost ? this.post.repost : this.post;
+		},
+		reactionsCount(): number {
+			return this.p.reaction_counts ? Object.keys(this.p.reaction_counts).map(key => this.p.reaction_counts[key]).reduce((a, b) => a + b) : 0;			
+		},
+		title(): string {
+			return dateStringify(this.p.created_at);
+		},
+		url(): string {
+			return `/${this.p.user.username}/${this.p.id}`;
+		}
+	},
+	created() {
+		this.connection = this.$root.$data.os.stream.getConnection();
+		this.connectionId = this.$root.$data.os.stream.use();
+	},
+	mounted() {
+		this.capture(true);
+
+		if (this.$root.$data.os.isSignedIn) {
+			this.connection.on('_connected_', this.onStreamConnected);
+		}
+
+		if (this.p.text) {
+			const tokens = this.p.ast;
+
+			this.$refs.text.innerHTML = this.$refs.text.innerHTML.replace('<p class="dummy"></p>', compile(tokens));
+
+			Array.from(this.$refs.text.children).forEach(e => {
+				if (e.tagName == 'MK-URL') riot.mount(e);
+			});
+
+			// URLをプレビュー
+			tokens
+			.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+			.map(t => {
+				riot.mount(this.$refs.text.appendChild(document.createElement('mk-url-preview')), {
+					url: t.url
+				});
+			});
+		}
+	},
+	beforeDestroy() {
+		this.decapture(true);
+		this.connection.off('_connected_', this.onStreamConnected);
+		this.$root.$data.os.stream.dispose(this.connectionId);
+	},
+	methods: {
+		capture(withHandler = false) {
+			if (this.$root.$data.os.isSignedIn) {
+				this.connection.send({
+					type: 'capture',
+					id: this.post.id
+				});
+				if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated);
+			}
+		},
+		decapture(withHandler = false) {
+			if (this.$root.$data.os.isSignedIn) {
+				this.connection.send({
+					type: 'decapture',
+					id: this.post.id
+				});
+				if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated);
+			}
+		},
+		onStreamConnected() {
+			this.capture();
+		},
+		onStreamPostUpdated(data) {
+			const post = data.post;
+			if (post.id == this.post.id) {
+				this.$emit('update:post', post);
+			}
+		}
+	}
+});
+</script>
+
+<script lang="typescript">
 
-this.mixin('stream');
-this.connection = this.stream.getConnection();
-this.connectionId = this.stream.use();
 
 this.isDetailOpened = false;
 
-this.set = post => {
-	this.post = post;
-	this.isRepost = this.post.repost && this.post.text == null && this.post.media_ids == null && this.post.poll == null;
-	this.p = this.isRepost ? this.post.repost : this.post;
-	this.p.reactions_count = this.p.reaction_counts ? Object.keys(this.p.reaction_counts).map(key => this.p.reaction_counts[key]).reduce((a, b) => a + b) : 0;
-	this.title = dateStringify(this.p.created_at);
-	this.url = `/${this.p.user.username}/${this.p.id}`;
-};
-
-this.set(this.opts.post);
-
-this.refresh = post => {
-	this.set(post);
-	this.update();
-	if (this.$refs.reactionsViewer) this.$refs.reactionsViewer.update({
-		post
-	});
-	if (this.$refs.pollViewer) this.$refs.pollViewer.init(post);
-};
-
-this.onStreamPostUpdated = data => {
-	const post = data.post;
-	if (post.id == this.post.id) {
-		this.refresh(post);
-	}
-};
-
-this.onStreamConnected = () => {
-	this.capture();
-};
-
-this.capture = withHandler => {
-	if (this.SIGNIN) {
-		this.connection.send({
-			type: 'capture',
-			id: this.post.id
-		});
-		if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated);
-	}
-};
-
-this.decapture = withHandler => {
-	if (this.SIGNIN) {
-		this.connection.send({
-			type: 'decapture',
-			id: this.post.id
-		});
-		if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated);
-	}
-};
-
-this.on('mount', () => {
-	this.capture(true);
-
-	if (this.SIGNIN) {
-		this.connection.on('_connected_', this.onStreamConnected);
-	}
-
-	if (this.p.text) {
-		const tokens = this.p.ast;
-
-		this.$refs.text.innerHTML = this.$refs.text.innerHTML.replace('<p class="dummy"></p>', compile(tokens));
-
-		Array.from(this.$refs.text.children).forEach(e => {
-			if (e.tagName == 'MK-URL') riot.mount(e);
-		});
-
-		// URLをプレビュー
-		tokens
-		.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
-		.map(t => {
-			riot.mount(this.$refs.text.appendChild(document.createElement('mk-url-preview')), {
-				url: t.url
-			});
-		});
-	}
-});
-
-this.on('unmount', () => {
-	this.decapture(true);
-	this.connection.off('_connected_', this.onStreamConnected);
-	this.stream.dispose(this.connectionId);
-});
-
 this.reply = () => {
 	riot.mount(document.body.appendChild(document.createElement('mk-post-form-window')), {
 		reply: this.p
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index 0e8b19f16..ba412848f 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-timeline" ref="root">
 	<template v-for="(post, i) in _posts">
-		<mk-timeline-post :post="post" :key="post.id"/>
+		<mk-timeline-post :post.sync="post" :key="post.id"/>
 		<p class="date" :key="post.id + '-time'" v-if="i != _posts.length - 1 && _post._date != _posts[i + 1]._date"><span>%fa:angle-up%{{ post._datetext }}</span><span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span></p>
 	</template>
 	<footer data-yield="footer">

From 5aa433d67aa9df65e58b1cbbbac3ccac51b34b34 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 18:00:08 +0900
Subject: [PATCH 040/286] wip

---
 .../views/components/timeline-post.vue        |  9 -------
 .../app/desktop/views/components/window.vue   | 25 +++++++++++++++++++
 2 files changed, 25 insertions(+), 9 deletions(-)
 create mode 100644 src/web/app/desktop/views/components/window.vue

diff --git a/src/web/app/desktop/views/components/timeline-post.vue b/src/web/app/desktop/views/components/timeline-post.vue
index 50c8ecf99..e4eaa8f79 100644
--- a/src/web/app/desktop/views/components/timeline-post.vue
+++ b/src/web/app/desktop/views/components/timeline-post.vue
@@ -172,9 +172,6 @@ export default Vue.extend({
 
 <script lang="typescript">
 
-
-this.isDetailOpened = false;
-
 this.reply = () => {
 	riot.mount(document.body.appendChild(document.createElement('mk-post-form-window')), {
 		reply: this.p
@@ -201,12 +198,6 @@ this.menu = () => {
 	});
 };
 
-this.toggleDetail = () => {
-	this.update({
-		isDetailOpened: !this.isDetailOpened
-	});
-};
-
 this.onKeyDown = e => {
 	let shouldBeCancel = true;
 
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
new file mode 100644
index 000000000..6961d9f08
--- /dev/null
+++ b/src/web/app/desktop/views/components/window.vue
@@ -0,0 +1,25 @@
+<template>
+<div :data-flexible="isFlexible" @dragover="onDragover">
+	<div class="bg" ref="bg" v-show="isModal" @click="onBgClick"></div>
+	<div class="main" ref="main" tabindex="-1" :data-is-modal="isModal" @mousedown="onBodyMousedown" @keydown="onKeydown">
+		<div class="body">
+			<header ref="header" @mousedown="onHeaderMousedown">
+				<h1 data-yield="header"><yield from="header"/></h1>
+				<div>
+					<button class="popout" v-if="popoutUrl" @mousedown="repelMove" @click="popout" title="ポップアウト">%fa:R window-restore%</button>
+					<button class="close" v-if="canClose" @mousedown="repelMove" @click="close" title="閉じる">%fa:times%</button>
+				</div>
+			</header>
+			<div class="content" data-yield="content"><yield from="content"/></div>
+		</div>
+		<div class="handle top" v-if="canResize" @mousedown="onTopHandleMousedown"></div>
+		<div class="handle right" v-if="canResize" @mousedown="onRightHandleMousedown"></div>
+		<div class="handle bottom" v-if="canResize" @mousedown="onBottomHandleMousedown"></div>
+		<div class="handle left" v-if="canResize" @mousedown="onLeftHandleMousedown"></div>
+		<div class="handle top-left" v-if="canResize" @mousedown="onTopLeftHandleMousedown"></div>
+		<div class="handle top-right" v-if="canResize" @mousedown="onTopRightHandleMousedown"></div>
+		<div class="handle bottom-right" v-if="canResize" @mousedown="onBottomRightHandleMousedown"></div>
+		<div class="handle bottom-left" v-if="canResize" @mousedown="onBottomLeftHandleMousedown"></div>
+	</div>
+</div>
+</template>

From a7f62e50f97167d5826ea6a3390aecd51ab7df73 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 18:38:12 +0900
Subject: [PATCH 041/286] wip

---
 src/web/app/desktop/-tags/window.tag          | 549 -----------------
 .../app/desktop/views/components/window.vue   | 558 +++++++++++++++++-
 2 files changed, 557 insertions(+), 550 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/window.tag

diff --git a/src/web/app/desktop/-tags/window.tag b/src/web/app/desktop/-tags/window.tag
deleted file mode 100644
index 051b43f07..000000000
--- a/src/web/app/desktop/-tags/window.tag
+++ /dev/null
@@ -1,549 +0,0 @@
-<mk-window data-flexible={ isFlexible } ondragover={ ondragover }>
-	<div class="bg" ref="bg" show={ isModal } @click="bgClick"></div>
-	<div class="main" ref="main" tabindex="-1" data-is-modal={ isModal } onmousedown={ onBodyMousedown } onkeydown={ onKeydown }>
-		<div class="body">
-			<header ref="header" onmousedown={ onHeaderMousedown }>
-				<h1 data-yield="header"><yield from="header"/></h1>
-				<div>
-					<button class="popout" v-if="popoutUrl" onmousedown={ repelMove } @click="popout" title="ポップアウト">%fa:R window-restore%</button>
-					<button class="close" v-if="canClose" onmousedown={ repelMove } @click="close" title="閉じる">%fa:times%</button>
-				</div>
-			</header>
-			<div class="content" data-yield="content"><yield from="content"/></div>
-		</div>
-		<div class="handle top" v-if="canResize" onmousedown={ onTopHandleMousedown }></div>
-		<div class="handle right" v-if="canResize" onmousedown={ onRightHandleMousedown }></div>
-		<div class="handle bottom" v-if="canResize" onmousedown={ onBottomHandleMousedown }></div>
-		<div class="handle left" v-if="canResize" onmousedown={ onLeftHandleMousedown }></div>
-		<div class="handle top-left" v-if="canResize" onmousedown={ onTopLeftHandleMousedown }></div>
-		<div class="handle top-right" v-if="canResize" onmousedown={ onTopRightHandleMousedown }></div>
-		<div class="handle bottom-right" v-if="canResize" onmousedown={ onBottomRightHandleMousedown }></div>
-		<div class="handle bottom-left" v-if="canResize" onmousedown={ onBottomLeftHandleMousedown }></div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> .bg
-				display block
-				position fixed
-				z-index 2048
-				top 0
-				left 0
-				width 100%
-				height 100%
-				background rgba(0, 0, 0, 0.7)
-				opacity 0
-				pointer-events none
-
-			> .main
-				display block
-				position fixed
-				z-index 2048
-				top 15%
-				left 0
-				margin 0
-				opacity 0
-				pointer-events none
-
-				&: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)
-
-				> .handle
-					$size = 8px
-
-					position absolute
-
-					&.top
-						top -($size)
-						left 0
-						width 100%
-						height $size
-						cursor ns-resize
-
-					&.right
-						top 0
-						right -($size)
-						width $size
-						height 100%
-						cursor ew-resize
-
-					&.bottom
-						bottom -($size)
-						left 0
-						width 100%
-						height $size
-						cursor ns-resize
-
-					&.left
-						top 0
-						left -($size)
-						width $size
-						height 100%
-						cursor ew-resize
-
-					&.top-left
-						top -($size)
-						left -($size)
-						width $size * 2
-						height $size * 2
-						cursor nwse-resize
-
-					&.top-right
-						top -($size)
-						right -($size)
-						width $size * 2
-						height $size * 2
-						cursor nesw-resize
-
-					&.bottom-right
-						bottom -($size)
-						right -($size)
-						width $size * 2
-						height $size * 2
-						cursor nwse-resize
-
-					&.bottom-left
-						bottom -($size)
-						left -($size)
-						width $size * 2
-						height $size * 2
-						cursor nesw-resize
-
-				> .body
-					height 100%
-					overflow hidden
-					background #fff
-					border-radius 6px
-					box-shadow 0 2px 6px 0 rgba(0, 0, 0, 0.2)
-
-					> header
-						$header-height = 40px
-
-						z-index 128
-						height $header-height
-						overflow hidden
-						white-space nowrap
-						cursor move
-						background #fff
-						border-radius 6px 6px 0 0
-						box-shadow 0 1px 0 rgba(#000, 0.1)
-
-						&, *
-							user-select none
-
-						> h1
-							pointer-events none
-							display block
-							margin 0 auto
-							overflow hidden
-							height $header-height
-							text-overflow ellipsis
-							text-align center
-							font-size 1em
-							line-height $header-height
-							font-weight normal
-							color #666
-
-						> div:last-child
-							position absolute
-							top 0
-							right 0
-							display block
-							z-index 1
-
-							> *
-								display inline-block
-								margin 0
-								padding 0
-								cursor pointer
-								font-size 1.2em
-								color rgba(#000, 0.4)
-								border none
-								outline none
-								background transparent
-
-								&:hover
-									color rgba(#000, 0.6)
-
-								&:active
-									color darken(#000, 30%)
-
-								> [data-fa]
-									padding 0
-									width $header-height
-									line-height $header-height
-									text-align center
-
-					> .content
-						height 100%
-
-			&:not([flexible])
-				> .main > .body > .content
-					height calc(100% - 40px)
-
-	</style>
-	<script lang="typescript">
-		import anime from 'animejs';
-		import contains from '../../common/scripts/contains';
-
-		this.minHeight = 40;
-		this.minWidth = 200;
-
-		this.isModal = this.opts.isModal != null ? this.opts.isModal : false;
-		this.canClose = this.opts.canClose != null ? this.opts.canClose : true;
-		this.popoutUrl = this.opts.popout;
-		this.isFlexible = this.opts.height == null;
-		this.canResize = !this.isFlexible;
-
-		this.on('mount', () => {
-			this.$refs.main.style.width = this.opts.width || '530px';
-			this.$refs.main.style.height = this.opts.height || 'auto';
-
-			this.$refs.main.style.top = '15%';
-			this.$refs.main.style.left = (window.innerWidth / 2) - (this.$refs.main.offsetWidth / 2) + 'px';
-
-			this.$refs.header.addEventListener('contextmenu', e => {
-				e.preventDefault();
-			});
-
-			window.addEventListener('resize', this.onBrowserResize);
-
-			this.open();
-		});
-
-		this.on('unmount', () => {
-			window.removeEventListener('resize', this.onBrowserResize);
-		});
-
-		this.onBrowserResize = () => {
-			const position = this.$refs.main.getBoundingClientRect();
-			const browserWidth = window.innerWidth;
-			const browserHeight = window.innerHeight;
-			const windowWidth = this.$refs.main.offsetWidth;
-			const windowHeight = this.$refs.main.offsetHeight;
-			if (position.left < 0) this.$refs.main.style.left = 0;
-			if (position.top < 0) this.$refs.main.style.top = 0;
-			if (position.left + windowWidth > browserWidth) this.$refs.main.style.left = browserWidth - windowWidth + 'px';
-			if (position.top + windowHeight > browserHeight) this.$refs.main.style.top = browserHeight - windowHeight + 'px';
-		};
-
-		this.open = () => {
-			this.$emit('opening');
-
-			this.top();
-
-			if (this.isModal) {
-				this.$refs.bg.style.pointerEvents = 'auto';
-				anime({
-					targets: this.$refs.bg,
-					opacity: 1,
-					duration: 100,
-					easing: 'linear'
-				});
-			}
-
-			this.$refs.main.style.pointerEvents = 'auto';
-			anime({
-				targets: this.$refs.main,
-				opacity: 1,
-				scale: [1.1, 1],
-				duration: 200,
-				easing: 'easeOutQuad'
-			});
-
-			//this.$refs.main.focus();
-
-			setTimeout(() => {
-				this.$emit('opened');
-			}, 300);
-		};
-
-		this.popout = () => {
-			const position = this.$refs.main.getBoundingClientRect();
-
-			const width = parseInt(getComputedStyle(this.$refs.main, '').width, 10);
-			const height = parseInt(getComputedStyle(this.$refs.main, '').height, 10);
-			const x = window.screenX + position.left;
-			const y = window.screenY + position.top;
-
-			const url = typeof this.popoutUrl == 'function' ? this.popoutUrl() : this.popoutUrl;
-
-			window.open(url, url,
-				`height=${height},width=${width},left=${x},top=${y}`);
-
-			this.close();
-		};
-
-		this.close = () => {
-			this.$emit('closing');
-
-			if (this.isModal) {
-				this.$refs.bg.style.pointerEvents = 'none';
-				anime({
-					targets: this.$refs.bg,
-					opacity: 0,
-					duration: 300,
-					easing: 'linear'
-				});
-			}
-
-			this.$refs.main.style.pointerEvents = 'none';
-
-			anime({
-				targets: this.$refs.main,
-				opacity: 0,
-				scale: 0.8,
-				duration: 300,
-				easing: [0.5, -0.5, 1, 0.5]
-			});
-
-			setTimeout(() => {
-				this.$emit('closed');
-			}, 300);
-		};
-
-		// 最前面へ移動します
-		this.top = () => {
-			let z = 0;
-
-			const ws = document.querySelectorAll('mk-window');
-			ws.forEach(w => {
-				if (w == this.root) return;
-				const m = w.querySelector(':scope > .main');
-				const mz = Number(document.defaultView.getComputedStyle(m, null).zIndex);
-				if (mz > z) z = mz;
-			});
-
-			if (z > 0) {
-				this.$refs.main.style.zIndex = z + 1;
-				if (this.isModal) this.$refs.bg.style.zIndex = z + 1;
-			}
-		};
-
-		this.repelMove = e => {
-			e.stopPropagation();
-			return true;
-		};
-
-		this.bgClick = () => {
-			if (this.canClose) this.close();
-		};
-
-		this.onBodyMousedown = () => {
-			this.top();
-		};
-
-		// ヘッダー掴み時
-		this.onHeaderMousedown = e => {
-			e.preventDefault();
-
-			if (!contains(this.$refs.main, document.activeElement)) this.$refs.main.focus();
-
-			const position = this.$refs.main.getBoundingClientRect();
-
-			const clickX = e.clientX;
-			const clickY = e.clientY;
-			const moveBaseX = clickX - position.left;
-			const moveBaseY = clickY - position.top;
-			const browserWidth = window.innerWidth;
-			const browserHeight = window.innerHeight;
-			const windowWidth = this.$refs.main.offsetWidth;
-			const windowHeight = this.$refs.main.offsetHeight;
-
-			// 動かした時
-			dragListen(me => {
-				let moveLeft = me.clientX - moveBaseX;
-				let moveTop = me.clientY - moveBaseY;
-
-				// 上はみ出し
-				if (moveTop < 0) moveTop = 0;
-
-				// 左はみ出し
-				if (moveLeft < 0) moveLeft = 0;
-
-				// 下はみ出し
-				if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight;
-
-				// 右はみ出し
-				if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
-
-				this.$refs.main.style.left = moveLeft + 'px';
-				this.$refs.main.style.top = moveTop + 'px';
-			});
-		};
-
-		// 上ハンドル掴み時
-		this.onTopHandleMousedown = e => {
-			e.preventDefault();
-
-			const base = e.clientY;
-			const height = parseInt(getComputedStyle(this.$refs.main, '').height, 10);
-			const top = parseInt(getComputedStyle(this.$refs.main, '').top, 10);
-
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientY - base;
-				if (top + move > 0) {
-					if (height + -move > this.minHeight) {
-						this.applyTransformHeight(height + -move);
-						this.applyTransformTop(top + move);
-					} else { // 最小の高さより小さくなろうとした時
-						this.applyTransformHeight(this.minHeight);
-						this.applyTransformTop(top + (height - this.minHeight));
-					}
-				} else { // 上のはみ出し時
-					this.applyTransformHeight(top + height);
-					this.applyTransformTop(0);
-				}
-			});
-		};
-
-		// 右ハンドル掴み時
-		this.onRightHandleMousedown = e => {
-			e.preventDefault();
-
-			const base = e.clientX;
-			const width = parseInt(getComputedStyle(this.$refs.main, '').width, 10);
-			const left = parseInt(getComputedStyle(this.$refs.main, '').left, 10);
-			const browserWidth = window.innerWidth;
-
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientX - base;
-				if (left + width + move < browserWidth) {
-					if (width + move > this.minWidth) {
-						this.applyTransformWidth(width + move);
-					} else { // 最小の幅より小さくなろうとした時
-						this.applyTransformWidth(this.minWidth);
-					}
-				} else { // 右のはみ出し時
-					this.applyTransformWidth(browserWidth - left);
-				}
-			});
-		};
-
-		// 下ハンドル掴み時
-		this.onBottomHandleMousedown = e => {
-			e.preventDefault();
-
-			const base = e.clientY;
-			const height = parseInt(getComputedStyle(this.$refs.main, '').height, 10);
-			const top = parseInt(getComputedStyle(this.$refs.main, '').top, 10);
-			const browserHeight = window.innerHeight;
-
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientY - base;
-				if (top + height + move < browserHeight) {
-					if (height + move > this.minHeight) {
-						this.applyTransformHeight(height + move);
-					} else { // 最小の高さより小さくなろうとした時
-						this.applyTransformHeight(this.minHeight);
-					}
-				} else { // 下のはみ出し時
-					this.applyTransformHeight(browserHeight - top);
-				}
-			});
-		};
-
-		// 左ハンドル掴み時
-		this.onLeftHandleMousedown = e => {
-			e.preventDefault();
-
-			const base = e.clientX;
-			const width = parseInt(getComputedStyle(this.$refs.main, '').width, 10);
-			const left = parseInt(getComputedStyle(this.$refs.main, '').left, 10);
-
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientX - base;
-				if (left + move > 0) {
-					if (width + -move > this.minWidth) {
-						this.applyTransformWidth(width + -move);
-						this.applyTransformLeft(left + move);
-					} else { // 最小の幅より小さくなろうとした時
-						this.applyTransformWidth(this.minWidth);
-						this.applyTransformLeft(left + (width - this.minWidth));
-					}
-				} else { // 左のはみ出し時
-					this.applyTransformWidth(left + width);
-					this.applyTransformLeft(0);
-				}
-			});
-		};
-
-		// 左上ハンドル掴み時
-		this.onTopLeftHandleMousedown = e => {
-			this.onTopHandleMousedown(e);
-			this.onLeftHandleMousedown(e);
-		};
-
-		// 右上ハンドル掴み時
-		this.onTopRightHandleMousedown = e => {
-			this.onTopHandleMousedown(e);
-			this.onRightHandleMousedown(e);
-		};
-
-		// 右下ハンドル掴み時
-		this.onBottomRightHandleMousedown = e => {
-			this.onBottomHandleMousedown(e);
-			this.onRightHandleMousedown(e);
-		};
-
-		// 左下ハンドル掴み時
-		this.onBottomLeftHandleMousedown = e => {
-			this.onBottomHandleMousedown(e);
-			this.onLeftHandleMousedown(e);
-		};
-
-		// 高さを適用
-		this.applyTransformHeight = height => {
-			this.$refs.main.style.height = height + 'px';
-		};
-
-		// 幅を適用
-		this.applyTransformWidth = width => {
-			this.$refs.main.style.width = width + 'px';
-		};
-
-		// Y座標を適用
-		this.applyTransformTop = top => {
-			this.$refs.main.style.top = top + 'px';
-		};
-
-		// X座標を適用
-		this.applyTransformLeft = left => {
-			this.$refs.main.style.left = left + 'px';
-		};
-
-		function dragListen(fn) {
-			window.addEventListener('mousemove',  fn);
-			window.addEventListener('mouseleave', dragClear.bind(null, fn));
-			window.addEventListener('mouseup',    dragClear.bind(null, fn));
-		}
-
-		function dragClear(fn) {
-			window.removeEventListener('mousemove',  fn);
-			window.removeEventListener('mouseleave', dragClear);
-			window.removeEventListener('mouseup',    dragClear);
-		}
-
-		this.ondragover = e => {
-			e.dataTransfer.dropEffect = 'none';
-		};
-
-		this.onKeydown = e => {
-			if (e.which == 27) { // Esc
-				if (this.canClose) {
-					e.preventDefault();
-					e.stopPropagation();
-					this.close();
-				}
-			}
-		};
-
-	</script>
-</mk-window>
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 6961d9f08..6c75918e0 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -1,5 +1,5 @@
 <template>
-<div :data-flexible="isFlexible" @dragover="onDragover">
+<div class="mk-window" :data-flexible="isFlexible" @dragover="onDragover">
 	<div class="bg" ref="bg" v-show="isModal" @click="onBgClick"></div>
 	<div class="main" ref="main" tabindex="-1" :data-is-modal="isModal" @mousedown="onBodyMousedown" @keydown="onKeydown">
 		<div class="body">
@@ -23,3 +23,559 @@
 	</div>
 </div>
 </template>
+
+<script lang="ts">
+import Vue from 'vue';
+import anime from 'animejs';
+import contains from '../../common/scripts/contains';
+
+const minHeight = 40;
+const minWidth = 200;
+
+export default Vue.extend({
+	props: {
+		isModal: {
+			type: Boolean,
+			default: false
+		},
+		canClose: {
+			type: Boolean,
+			default: true
+		},
+		height: {
+			type: Number
+		},
+		popoutUrl: {
+			type: String
+		}
+	},
+	computed: {
+		isFlexible(): boolean {
+			return this.height == null;
+		},
+		canResize(): boolean {
+			return !this.isFlexible;
+		}
+	}
+});
+</script>
+
+
+<script lang="typescript">
+
+this.on('mount', () => {
+	this.$refs.main.style.width = this.opts.width || '530px';
+	this.$refs.main.style.height = this.opts.height || 'auto';
+
+	this.$refs.main.style.top = '15%';
+	this.$refs.main.style.left = (window.innerWidth / 2) - (this.$refs.main.offsetWidth / 2) + 'px';
+
+	this.$refs.header.addEventListener('contextmenu', e => {
+		e.preventDefault();
+	});
+
+	window.addEventListener('resize', this.onBrowserResize);
+
+	this.open();
+});
+
+this.on('unmount', () => {
+	window.removeEventListener('resize', this.onBrowserResize);
+});
+
+this.onBrowserResize = () => {
+	const position = this.$refs.main.getBoundingClientRect();
+	const browserWidth = window.innerWidth;
+	const browserHeight = window.innerHeight;
+	const windowWidth = this.$refs.main.offsetWidth;
+	const windowHeight = this.$refs.main.offsetHeight;
+	if (position.left < 0) this.$refs.main.style.left = 0;
+	if (position.top < 0) this.$refs.main.style.top = 0;
+	if (position.left + windowWidth > browserWidth) this.$refs.main.style.left = browserWidth - windowWidth + 'px';
+	if (position.top + windowHeight > browserHeight) this.$refs.main.style.top = browserHeight - windowHeight + 'px';
+};
+
+this.open = () => {
+	this.$emit('opening');
+
+	this.top();
+
+	if (this.isModal) {
+		this.$refs.bg.style.pointerEvents = 'auto';
+		anime({
+			targets: this.$refs.bg,
+			opacity: 1,
+			duration: 100,
+			easing: 'linear'
+		});
+	}
+
+	this.$refs.main.style.pointerEvents = 'auto';
+	anime({
+		targets: this.$refs.main,
+		opacity: 1,
+		scale: [1.1, 1],
+		duration: 200,
+		easing: 'easeOutQuad'
+	});
+
+	//this.$refs.main.focus();
+
+	setTimeout(() => {
+		this.$emit('opened');
+	}, 300);
+};
+
+this.popout = () => {
+	const position = this.$refs.main.getBoundingClientRect();
+
+	const width = parseInt(getComputedStyle(this.$refs.main, '').width, 10);
+	const height = parseInt(getComputedStyle(this.$refs.main, '').height, 10);
+	const x = window.screenX + position.left;
+	const y = window.screenY + position.top;
+
+	const url = typeof this.popoutUrl == 'function' ? this.popoutUrl() : this.popoutUrl;
+
+	window.open(url, url,
+		`height=${height},width=${width},left=${x},top=${y}`);
+
+	this.close();
+};
+
+this.close = () => {
+	this.$emit('closing');
+
+	if (this.isModal) {
+		this.$refs.bg.style.pointerEvents = 'none';
+		anime({
+			targets: this.$refs.bg,
+			opacity: 0,
+			duration: 300,
+			easing: 'linear'
+		});
+	}
+
+	this.$refs.main.style.pointerEvents = 'none';
+
+	anime({
+		targets: this.$refs.main,
+		opacity: 0,
+		scale: 0.8,
+		duration: 300,
+		easing: [0.5, -0.5, 1, 0.5]
+	});
+
+	setTimeout(() => {
+		this.$emit('closed');
+	}, 300);
+};
+
+// 最前面へ移動します
+this.top = () => {
+	let z = 0;
+
+	const ws = document.querySelectorAll('mk-window');
+	ws.forEach(w => {
+		if (w == this.root) return;
+		const m = w.querySelector(':scope > .main');
+		const mz = Number(document.defaultView.getComputedStyle(m, null).zIndex);
+		if (mz > z) z = mz;
+	});
+
+	if (z > 0) {
+		this.$refs.main.style.zIndex = z + 1;
+		if (this.isModal) this.$refs.bg.style.zIndex = z + 1;
+	}
+};
+
+this.repelMove = e => {
+	e.stopPropagation();
+	return true;
+};
+
+this.bgClick = () => {
+	if (this.canClose) this.close();
+};
+
+this.onBodyMousedown = () => {
+	this.top();
+};
+
+// ヘッダー掴み時
+this.onHeaderMousedown = e => {
+	e.preventDefault();
+
+	if (!contains(this.$refs.main, document.activeElement)) this.$refs.main.focus();
+
+	const position = this.$refs.main.getBoundingClientRect();
+
+	const clickX = e.clientX;
+	const clickY = e.clientY;
+	const moveBaseX = clickX - position.left;
+	const moveBaseY = clickY - position.top;
+	const browserWidth = window.innerWidth;
+	const browserHeight = window.innerHeight;
+	const windowWidth = this.$refs.main.offsetWidth;
+	const windowHeight = this.$refs.main.offsetHeight;
+
+	// 動かした時
+	dragListen(me => {
+		let moveLeft = me.clientX - moveBaseX;
+		let moveTop = me.clientY - moveBaseY;
+
+		// 上はみ出し
+		if (moveTop < 0) moveTop = 0;
+
+		// 左はみ出し
+		if (moveLeft < 0) moveLeft = 0;
+
+		// 下はみ出し
+		if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight;
+
+		// 右はみ出し
+		if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
+
+		this.$refs.main.style.left = moveLeft + 'px';
+		this.$refs.main.style.top = moveTop + 'px';
+	});
+};
+
+// 上ハンドル掴み時
+this.onTopHandleMousedown = e => {
+	e.preventDefault();
+
+	const base = e.clientY;
+	const height = parseInt(getComputedStyle(this.$refs.main, '').height, 10);
+	const top = parseInt(getComputedStyle(this.$refs.main, '').top, 10);
+
+	// 動かした時
+	dragListen(me => {
+		const move = me.clientY - base;
+		if (top + move > 0) {
+			if (height + -move > this.minHeight) {
+				this.applyTransformHeight(height + -move);
+				this.applyTransformTop(top + move);
+			} else { // 最小の高さより小さくなろうとした時
+				this.applyTransformHeight(this.minHeight);
+				this.applyTransformTop(top + (height - this.minHeight));
+			}
+		} else { // 上のはみ出し時
+			this.applyTransformHeight(top + height);
+			this.applyTransformTop(0);
+		}
+	});
+};
+
+// 右ハンドル掴み時
+this.onRightHandleMousedown = e => {
+	e.preventDefault();
+
+	const base = e.clientX;
+	const width = parseInt(getComputedStyle(this.$refs.main, '').width, 10);
+	const left = parseInt(getComputedStyle(this.$refs.main, '').left, 10);
+	const browserWidth = window.innerWidth;
+
+	// 動かした時
+	dragListen(me => {
+		const move = me.clientX - base;
+		if (left + width + move < browserWidth) {
+			if (width + move > this.minWidth) {
+				this.applyTransformWidth(width + move);
+			} else { // 最小の幅より小さくなろうとした時
+				this.applyTransformWidth(this.minWidth);
+			}
+		} else { // 右のはみ出し時
+			this.applyTransformWidth(browserWidth - left);
+		}
+	});
+};
+
+// 下ハンドル掴み時
+this.onBottomHandleMousedown = e => {
+	e.preventDefault();
+
+	const base = e.clientY;
+	const height = parseInt(getComputedStyle(this.$refs.main, '').height, 10);
+	const top = parseInt(getComputedStyle(this.$refs.main, '').top, 10);
+	const browserHeight = window.innerHeight;
+
+	// 動かした時
+	dragListen(me => {
+		const move = me.clientY - base;
+		if (top + height + move < browserHeight) {
+			if (height + move > this.minHeight) {
+				this.applyTransformHeight(height + move);
+			} else { // 最小の高さより小さくなろうとした時
+				this.applyTransformHeight(this.minHeight);
+			}
+		} else { // 下のはみ出し時
+			this.applyTransformHeight(browserHeight - top);
+		}
+	});
+};
+
+// 左ハンドル掴み時
+this.onLeftHandleMousedown = e => {
+	e.preventDefault();
+
+	const base = e.clientX;
+	const width = parseInt(getComputedStyle(this.$refs.main, '').width, 10);
+	const left = parseInt(getComputedStyle(this.$refs.main, '').left, 10);
+
+	// 動かした時
+	dragListen(me => {
+		const move = me.clientX - base;
+		if (left + move > 0) {
+			if (width + -move > this.minWidth) {
+				this.applyTransformWidth(width + -move);
+				this.applyTransformLeft(left + move);
+			} else { // 最小の幅より小さくなろうとした時
+				this.applyTransformWidth(this.minWidth);
+				this.applyTransformLeft(left + (width - this.minWidth));
+			}
+		} else { // 左のはみ出し時
+			this.applyTransformWidth(left + width);
+			this.applyTransformLeft(0);
+		}
+	});
+};
+
+// 左上ハンドル掴み時
+this.onTopLeftHandleMousedown = e => {
+	this.onTopHandleMousedown(e);
+	this.onLeftHandleMousedown(e);
+};
+
+// 右上ハンドル掴み時
+this.onTopRightHandleMousedown = e => {
+	this.onTopHandleMousedown(e);
+	this.onRightHandleMousedown(e);
+};
+
+// 右下ハンドル掴み時
+this.onBottomRightHandleMousedown = e => {
+	this.onBottomHandleMousedown(e);
+	this.onRightHandleMousedown(e);
+};
+
+// 左下ハンドル掴み時
+this.onBottomLeftHandleMousedown = e => {
+	this.onBottomHandleMousedown(e);
+	this.onLeftHandleMousedown(e);
+};
+
+// 高さを適用
+this.applyTransformHeight = height => {
+	this.$refs.main.style.height = height + 'px';
+};
+
+// 幅を適用
+this.applyTransformWidth = width => {
+	this.$refs.main.style.width = width + 'px';
+};
+
+// Y座標を適用
+this.applyTransformTop = top => {
+	this.$refs.main.style.top = top + 'px';
+};
+
+// X座標を適用
+this.applyTransformLeft = left => {
+	this.$refs.main.style.left = left + 'px';
+};
+
+function dragListen(fn) {
+	window.addEventListener('mousemove',  fn);
+	window.addEventListener('mouseleave', dragClear.bind(null, fn));
+	window.addEventListener('mouseup',    dragClear.bind(null, fn));
+}
+
+function dragClear(fn) {
+	window.removeEventListener('mousemove',  fn);
+	window.removeEventListener('mouseleave', dragClear);
+	window.removeEventListener('mouseup',    dragClear);
+}
+
+this.ondragover = e => {
+	e.dataTransfer.dropEffect = 'none';
+};
+
+this.onKeydown = e => {
+	if (e.which == 27) { // Esc
+		if (this.canClose) {
+			e.preventDefault();
+			e.stopPropagation();
+			this.close();
+		}
+	}
+};
+
+</script>
+
+
+<style lang="stylus" scoped>
+.mk-window
+	display block
+
+	> .bg
+		display block
+		position fixed
+		z-index 2048
+		top 0
+		left 0
+		width 100%
+		height 100%
+		background rgba(0, 0, 0, 0.7)
+		opacity 0
+		pointer-events none
+
+	> .main
+		display block
+		position fixed
+		z-index 2048
+		top 15%
+		left 0
+		margin 0
+		opacity 0
+		pointer-events none
+
+		&: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)
+
+		> .handle
+			$size = 8px
+
+			position absolute
+
+			&.top
+				top -($size)
+				left 0
+				width 100%
+				height $size
+				cursor ns-resize
+
+			&.right
+				top 0
+				right -($size)
+				width $size
+				height 100%
+				cursor ew-resize
+
+			&.bottom
+				bottom -($size)
+				left 0
+				width 100%
+				height $size
+				cursor ns-resize
+
+			&.left
+				top 0
+				left -($size)
+				width $size
+				height 100%
+				cursor ew-resize
+
+			&.top-left
+				top -($size)
+				left -($size)
+				width $size * 2
+				height $size * 2
+				cursor nwse-resize
+
+			&.top-right
+				top -($size)
+				right -($size)
+				width $size * 2
+				height $size * 2
+				cursor nesw-resize
+
+			&.bottom-right
+				bottom -($size)
+				right -($size)
+				width $size * 2
+				height $size * 2
+				cursor nwse-resize
+
+			&.bottom-left
+				bottom -($size)
+				left -($size)
+				width $size * 2
+				height $size * 2
+				cursor nesw-resize
+
+		> .body
+			height 100%
+			overflow hidden
+			background #fff
+			border-radius 6px
+			box-shadow 0 2px 6px 0 rgba(0, 0, 0, 0.2)
+
+			> header
+				$header-height = 40px
+
+				z-index 128
+				height $header-height
+				overflow hidden
+				white-space nowrap
+				cursor move
+				background #fff
+				border-radius 6px 6px 0 0
+				box-shadow 0 1px 0 rgba(#000, 0.1)
+
+				&, *
+					user-select none
+
+				> h1
+					pointer-events none
+					display block
+					margin 0 auto
+					overflow hidden
+					height $header-height
+					text-overflow ellipsis
+					text-align center
+					font-size 1em
+					line-height $header-height
+					font-weight normal
+					color #666
+
+				> div:last-child
+					position absolute
+					top 0
+					right 0
+					display block
+					z-index 1
+
+					> *
+						display inline-block
+						margin 0
+						padding 0
+						cursor pointer
+						font-size 1.2em
+						color rgba(#000, 0.4)
+						border none
+						outline none
+						background transparent
+
+						&:hover
+							color rgba(#000, 0.6)
+
+						&:active
+							color darken(#000, 30%)
+
+						> [data-fa]
+							padding 0
+							width $header-height
+							line-height $header-height
+							text-align center
+
+			> .content
+				height 100%
+
+	&:not([flexible])
+		> .main > .body > .content
+			height calc(100% - 40px)
+
+</style>
+

From 6cf1fdbf826fcaabefd3367ec595e12533545fc1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 18:50:30 +0900
Subject: [PATCH 042/286] wip

---
 src/web/app/desktop/views/components/window.vue | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 6c75918e0..4a9aa900c 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-window" :data-flexible="isFlexible" @dragover="onDragover">
 	<div class="bg" ref="bg" v-show="isModal" @click="onBgClick"></div>
-	<div class="main" ref="main" tabindex="-1" :data-is-modal="isModal" @mousedown="onBodyMousedown" @keydown="onKeydown">
+	<div class="main" ref="main" tabindex="-1" :data-is-modal="isModal" @mousedown="onBodyMousedown" @keydown="onKeydown" :style="{ width, height }">
 		<div class="body">
 			<header ref="header" @mousedown="onHeaderMousedown">
 				<h1 data-yield="header"><yield from="header"/></h1>
@@ -42,8 +42,13 @@ export default Vue.extend({
 			type: Boolean,
 			default: true
 		},
+		width: {
+			type: String,
+			default: '530px'
+		},
 		height: {
-			type: Number
+			type: String,
+			default: 'auto'
 		},
 		popoutUrl: {
 			type: String
@@ -56,6 +61,9 @@ export default Vue.extend({
 		canResize(): boolean {
 			return !this.isFlexible;
 		}
+	},
+	mounted() {
+
 	}
 });
 </script>
@@ -64,8 +72,6 @@ export default Vue.extend({
 <script lang="typescript">
 
 this.on('mount', () => {
-	this.$refs.main.style.width = this.opts.width || '530px';
-	this.$refs.main.style.height = this.opts.height || 'auto';
 
 	this.$refs.main.style.top = '15%';
 	this.$refs.main.style.left = (window.innerWidth / 2) - (this.$refs.main.offsetWidth / 2) + 'px';

From a0f3182a0a940602a0b94ab42c488144eba0ec63 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 22:04:08 +0900
Subject: [PATCH 043/286] wip

---
 src/web/app/common/mios.ts                    |  21 +
 .../app/desktop/views/components/window.vue   | 681 +++++++++---------
 2 files changed, 361 insertions(+), 341 deletions(-)

diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index e91def521..550d9e6bf 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -79,6 +79,11 @@ export default class MiOS extends EventEmitter {
 	 */
 	private shouldRegisterSw: boolean;
 
+	/**
+	 * ウィンドウシステム
+	 */
+	public windows = new WindowSystem();
+
 	/**
 	 * MiOSインスタンスを作成します
 	 * @param shouldRegisterSw ServiceWorkerを登録するかどうか
@@ -359,6 +364,22 @@ export default class MiOS extends EventEmitter {
 	}
 }
 
+class WindowSystem {
+	private windows = new Set();
+
+	public add(window) {
+		this.windows.add(window);
+	}
+
+	public remove(window) {
+		this.windows.delete(window);
+	}
+
+	public getAll() {
+		return this.windows;
+	}
+}
+
 /**
  * Convert the URL safe base64 string to a Uint8Array
  * @param base64String base64 string
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 4a9aa900c..ac3af3a57 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -3,23 +3,23 @@
 	<div class="bg" ref="bg" v-show="isModal" @click="onBgClick"></div>
 	<div class="main" ref="main" tabindex="-1" :data-is-modal="isModal" @mousedown="onBodyMousedown" @keydown="onKeydown" :style="{ width, height }">
 		<div class="body">
-			<header ref="header" @mousedown="onHeaderMousedown">
+			<header ref="header" @contextmenu.prevent="() => {}" @mousedown.prevent="onHeaderMousedown">
 				<h1 data-yield="header"><yield from="header"/></h1>
 				<div>
-					<button class="popout" v-if="popoutUrl" @mousedown="repelMove" @click="popout" title="ポップアウト">%fa:R window-restore%</button>
-					<button class="close" v-if="canClose" @mousedown="repelMove" @click="close" title="閉じる">%fa:times%</button>
+					<button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="ポップアウト">%fa:R window-restore%</button>
+					<button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="閉じる">%fa:times%</button>
 				</div>
 			</header>
 			<div class="content" data-yield="content"><yield from="content"/></div>
 		</div>
-		<div class="handle top" v-if="canResize" @mousedown="onTopHandleMousedown"></div>
-		<div class="handle right" v-if="canResize" @mousedown="onRightHandleMousedown"></div>
-		<div class="handle bottom" v-if="canResize" @mousedown="onBottomHandleMousedown"></div>
-		<div class="handle left" v-if="canResize" @mousedown="onLeftHandleMousedown"></div>
-		<div class="handle top-left" v-if="canResize" @mousedown="onTopLeftHandleMousedown"></div>
-		<div class="handle top-right" v-if="canResize" @mousedown="onTopRightHandleMousedown"></div>
-		<div class="handle bottom-right" v-if="canResize" @mousedown="onBottomRightHandleMousedown"></div>
-		<div class="handle bottom-left" v-if="canResize" @mousedown="onBottomLeftHandleMousedown"></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>
 	</div>
 </div>
 </template>
@@ -32,6 +32,18 @@ import contains from '../../common/scripts/contains';
 const minHeight = 40;
 const minWidth = 200;
 
+function dragListen(fn) {
+	window.addEventListener('mousemove',  fn);
+	window.addEventListener('mouseleave', dragClear.bind(null, fn));
+	window.addEventListener('mouseup',    dragClear.bind(null, fn));
+}
+
+function dragClear(fn) {
+	window.removeEventListener('mousemove',  fn);
+	window.removeEventListener('mouseleave', dragClear);
+	window.removeEventListener('mouseup',    dragClear);
+}
+
 export default Vue.extend({
 	props: {
 		isModal: {
@@ -54,6 +66,7 @@ export default Vue.extend({
 			type: String
 		}
 	},
+
 	computed: {
 		isFlexible(): boolean {
 			return this.height == null;
@@ -62,363 +75,350 @@ export default Vue.extend({
 			return !this.isFlexible;
 		}
 	},
+
+	created() {
+		// ウィンドウをウィンドウシステムに登録
+		this.$root.$data.os.windows.add(this);
+	},
+
 	mounted() {
+		const main = this.$refs.main as any;
+		main.style.top = '15%';
+		main.style.left = (window.innerWidth / 2) - (main.offsetWidth / 2) + 'px';
 
-	}
-});
-</script>
+		window.addEventListener('resize', this.onBrowserResize);
 
+		this.open();
+	},
 
-<script lang="typescript">
+	destroyed() {
+		// ウィンドウをウィンドウシステムから削除
+		this.$root.$data.os.windows.remove(this);
 
-this.on('mount', () => {
+		window.removeEventListener('resize', this.onBrowserResize);
+	},
 
-	this.$refs.main.style.top = '15%';
-	this.$refs.main.style.left = (window.innerWidth / 2) - (this.$refs.main.offsetWidth / 2) + 'px';
+	methods: {
+		open() {
+			this.$emit('opening');
 
-	this.$refs.header.addEventListener('contextmenu', e => {
-		e.preventDefault();
-	});
+			this.top();
 
-	window.addEventListener('resize', this.onBrowserResize);
+			const bg = this.$refs.bg as any;
+			const main = this.$refs.main as any;
 
-	this.open();
-});
-
-this.on('unmount', () => {
-	window.removeEventListener('resize', this.onBrowserResize);
-});
-
-this.onBrowserResize = () => {
-	const position = this.$refs.main.getBoundingClientRect();
-	const browserWidth = window.innerWidth;
-	const browserHeight = window.innerHeight;
-	const windowWidth = this.$refs.main.offsetWidth;
-	const windowHeight = this.$refs.main.offsetHeight;
-	if (position.left < 0) this.$refs.main.style.left = 0;
-	if (position.top < 0) this.$refs.main.style.top = 0;
-	if (position.left + windowWidth > browserWidth) this.$refs.main.style.left = browserWidth - windowWidth + 'px';
-	if (position.top + windowHeight > browserHeight) this.$refs.main.style.top = browserHeight - windowHeight + 'px';
-};
-
-this.open = () => {
-	this.$emit('opening');
-
-	this.top();
-
-	if (this.isModal) {
-		this.$refs.bg.style.pointerEvents = 'auto';
-		anime({
-			targets: this.$refs.bg,
-			opacity: 1,
-			duration: 100,
-			easing: 'linear'
-		});
-	}
-
-	this.$refs.main.style.pointerEvents = 'auto';
-	anime({
-		targets: this.$refs.main,
-		opacity: 1,
-		scale: [1.1, 1],
-		duration: 200,
-		easing: 'easeOutQuad'
-	});
-
-	//this.$refs.main.focus();
-
-	setTimeout(() => {
-		this.$emit('opened');
-	}, 300);
-};
-
-this.popout = () => {
-	const position = this.$refs.main.getBoundingClientRect();
-
-	const width = parseInt(getComputedStyle(this.$refs.main, '').width, 10);
-	const height = parseInt(getComputedStyle(this.$refs.main, '').height, 10);
-	const x = window.screenX + position.left;
-	const y = window.screenY + position.top;
-
-	const url = typeof this.popoutUrl == 'function' ? this.popoutUrl() : this.popoutUrl;
-
-	window.open(url, url,
-		`height=${height},width=${width},left=${x},top=${y}`);
-
-	this.close();
-};
-
-this.close = () => {
-	this.$emit('closing');
-
-	if (this.isModal) {
-		this.$refs.bg.style.pointerEvents = 'none';
-		anime({
-			targets: this.$refs.bg,
-			opacity: 0,
-			duration: 300,
-			easing: 'linear'
-		});
-	}
-
-	this.$refs.main.style.pointerEvents = 'none';
-
-	anime({
-		targets: this.$refs.main,
-		opacity: 0,
-		scale: 0.8,
-		duration: 300,
-		easing: [0.5, -0.5, 1, 0.5]
-	});
-
-	setTimeout(() => {
-		this.$emit('closed');
-	}, 300);
-};
-
-// 最前面へ移動します
-this.top = () => {
-	let z = 0;
-
-	const ws = document.querySelectorAll('mk-window');
-	ws.forEach(w => {
-		if (w == this.root) return;
-		const m = w.querySelector(':scope > .main');
-		const mz = Number(document.defaultView.getComputedStyle(m, null).zIndex);
-		if (mz > z) z = mz;
-	});
-
-	if (z > 0) {
-		this.$refs.main.style.zIndex = z + 1;
-		if (this.isModal) this.$refs.bg.style.zIndex = z + 1;
-	}
-};
-
-this.repelMove = e => {
-	e.stopPropagation();
-	return true;
-};
-
-this.bgClick = () => {
-	if (this.canClose) this.close();
-};
-
-this.onBodyMousedown = () => {
-	this.top();
-};
-
-// ヘッダー掴み時
-this.onHeaderMousedown = e => {
-	e.preventDefault();
-
-	if (!contains(this.$refs.main, document.activeElement)) this.$refs.main.focus();
-
-	const position = this.$refs.main.getBoundingClientRect();
-
-	const clickX = e.clientX;
-	const clickY = e.clientY;
-	const moveBaseX = clickX - position.left;
-	const moveBaseY = clickY - position.top;
-	const browserWidth = window.innerWidth;
-	const browserHeight = window.innerHeight;
-	const windowWidth = this.$refs.main.offsetWidth;
-	const windowHeight = this.$refs.main.offsetHeight;
-
-	// 動かした時
-	dragListen(me => {
-		let moveLeft = me.clientX - moveBaseX;
-		let moveTop = me.clientY - moveBaseY;
-
-		// 上はみ出し
-		if (moveTop < 0) moveTop = 0;
-
-		// 左はみ出し
-		if (moveLeft < 0) moveLeft = 0;
-
-		// 下はみ出し
-		if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight;
-
-		// 右はみ出し
-		if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
-
-		this.$refs.main.style.left = moveLeft + 'px';
-		this.$refs.main.style.top = moveTop + 'px';
-	});
-};
-
-// 上ハンドル掴み時
-this.onTopHandleMousedown = e => {
-	e.preventDefault();
-
-	const base = e.clientY;
-	const height = parseInt(getComputedStyle(this.$refs.main, '').height, 10);
-	const top = parseInt(getComputedStyle(this.$refs.main, '').top, 10);
-
-	// 動かした時
-	dragListen(me => {
-		const move = me.clientY - base;
-		if (top + move > 0) {
-			if (height + -move > this.minHeight) {
-				this.applyTransformHeight(height + -move);
-				this.applyTransformTop(top + move);
-			} else { // 最小の高さより小さくなろうとした時
-				this.applyTransformHeight(this.minHeight);
-				this.applyTransformTop(top + (height - this.minHeight));
+			if (this.isModal) {
+				bg.style.pointerEvents = 'auto';
+				anime({
+					targets: bg,
+					opacity: 1,
+					duration: 100,
+					easing: 'linear'
+				});
 			}
-		} else { // 上のはみ出し時
-			this.applyTransformHeight(top + height);
-			this.applyTransformTop(0);
-		}
-	});
-};
 
-// 右ハンドル掴み時
-this.onRightHandleMousedown = e => {
-	e.preventDefault();
+			main.style.pointerEvents = 'auto';
+			anime({
+				targets: main,
+				opacity: 1,
+				scale: [1.1, 1],
+				duration: 200,
+				easing: 'easeOutQuad'
+			});
 
-	const base = e.clientX;
-	const width = parseInt(getComputedStyle(this.$refs.main, '').width, 10);
-	const left = parseInt(getComputedStyle(this.$refs.main, '').left, 10);
-	const browserWidth = window.innerWidth;
+			if (focus) main.focus();
 
-	// 動かした時
-	dragListen(me => {
-		const move = me.clientX - base;
-		if (left + width + move < browserWidth) {
-			if (width + move > this.minWidth) {
-				this.applyTransformWidth(width + move);
-			} else { // 最小の幅より小さくなろうとした時
-				this.applyTransformWidth(this.minWidth);
+			setTimeout(() => {
+				this.$emit('opened');
+			}, 300);
+		},
+
+		close() {
+			this.$emit('closing');
+
+			const bg = this.$refs.bg as any;
+			const main = this.$refs.main as any;
+
+			if (this.isModal) {
+				bg.style.pointerEvents = 'none';
+				anime({
+					targets: bg,
+					opacity: 0,
+					duration: 300,
+					easing: 'linear'
+				});
 			}
-		} else { // 右のはみ出し時
-			this.applyTransformWidth(browserWidth - left);
-		}
-	});
-};
 
-// 下ハンドル掴み時
-this.onBottomHandleMousedown = e => {
-	e.preventDefault();
+			main.style.pointerEvents = 'none';
 
-	const base = e.clientY;
-	const height = parseInt(getComputedStyle(this.$refs.main, '').height, 10);
-	const top = parseInt(getComputedStyle(this.$refs.main, '').top, 10);
-	const browserHeight = window.innerHeight;
+			anime({
+				targets: main,
+				opacity: 0,
+				scale: 0.8,
+				duration: 300,
+				easing: [0.5, -0.5, 1, 0.5]
+			});
 
-	// 動かした時
-	dragListen(me => {
-		const move = me.clientY - base;
-		if (top + height + move < browserHeight) {
-			if (height + move > this.minHeight) {
-				this.applyTransformHeight(height + move);
-			} else { // 最小の高さより小さくなろうとした時
-				this.applyTransformHeight(this.minHeight);
-			}
-		} else { // 下のはみ出し時
-			this.applyTransformHeight(browserHeight - top);
-		}
-	});
-};
+			setTimeout(() => {
+				this.$emit('closed');
+			}, 300);
+		},
 
-// 左ハンドル掴み時
-this.onLeftHandleMousedown = e => {
-	e.preventDefault();
+		popout() {
+			const main = this.$refs.main as any;
 
-	const base = e.clientX;
-	const width = parseInt(getComputedStyle(this.$refs.main, '').width, 10);
-	const left = parseInt(getComputedStyle(this.$refs.main, '').left, 10);
+			const position = main.getBoundingClientRect();
 
-	// 動かした時
-	dragListen(me => {
-		const move = me.clientX - base;
-		if (left + move > 0) {
-			if (width + -move > this.minWidth) {
-				this.applyTransformWidth(width + -move);
-				this.applyTransformLeft(left + move);
-			} else { // 最小の幅より小さくなろうとした時
-				this.applyTransformWidth(this.minWidth);
-				this.applyTransformLeft(left + (width - this.minWidth));
-			}
-		} else { // 左のはみ出し時
-			this.applyTransformWidth(left + width);
-			this.applyTransformLeft(0);
-		}
-	});
-};
+			const width = parseInt(getComputedStyle(main, '').width, 10);
+			const height = parseInt(getComputedStyle(main, '').height, 10);
+			const x = window.screenX + position.left;
+			const y = window.screenY + position.top;
 
-// 左上ハンドル掴み時
-this.onTopLeftHandleMousedown = e => {
-	this.onTopHandleMousedown(e);
-	this.onLeftHandleMousedown(e);
-};
+			const url = typeof this.popoutUrl == 'function' ? this.popoutUrl() : this.popoutUrl;
 
-// 右上ハンドル掴み時
-this.onTopRightHandleMousedown = e => {
-	this.onTopHandleMousedown(e);
-	this.onRightHandleMousedown(e);
-};
+			window.open(url, url,
+				`height=${height}, width=${width}, left=${x}, top=${y}`);
 
-// 右下ハンドル掴み時
-this.onBottomRightHandleMousedown = e => {
-	this.onBottomHandleMousedown(e);
-	this.onRightHandleMousedown(e);
-};
-
-// 左下ハンドル掴み時
-this.onBottomLeftHandleMousedown = e => {
-	this.onBottomHandleMousedown(e);
-	this.onLeftHandleMousedown(e);
-};
-
-// 高さを適用
-this.applyTransformHeight = height => {
-	this.$refs.main.style.height = height + 'px';
-};
-
-// 幅を適用
-this.applyTransformWidth = width => {
-	this.$refs.main.style.width = width + 'px';
-};
-
-// Y座標を適用
-this.applyTransformTop = top => {
-	this.$refs.main.style.top = top + 'px';
-};
-
-// X座標を適用
-this.applyTransformLeft = left => {
-	this.$refs.main.style.left = left + 'px';
-};
-
-function dragListen(fn) {
-	window.addEventListener('mousemove',  fn);
-	window.addEventListener('mouseleave', dragClear.bind(null, fn));
-	window.addEventListener('mouseup',    dragClear.bind(null, fn));
-}
-
-function dragClear(fn) {
-	window.removeEventListener('mousemove',  fn);
-	window.removeEventListener('mouseleave', dragClear);
-	window.removeEventListener('mouseup',    dragClear);
-}
-
-this.ondragover = e => {
-	e.dataTransfer.dropEffect = 'none';
-};
-
-this.onKeydown = e => {
-	if (e.which == 27) { // Esc
-		if (this.canClose) {
-			e.preventDefault();
-			e.stopPropagation();
 			this.close();
+		},
+
+		// 最前面へ移動
+		top() {
+			let z = 0;
+
+			this.$root.$data.os.windows.getAll().forEach(w => {
+				if (w == this) return;
+				const m = w.$refs.main;
+				const mz = Number(document.defaultView.getComputedStyle(m, null).zIndex);
+				if (mz > z) z = mz;
+			});
+
+			if (z > 0) {
+				(this.$refs.main as any).style.zIndex = z + 1;
+				if (this.isModal) (this.$refs.bg as any).style.zIndex = z + 1;
+			}
+		},
+
+		onBgClick() {
+			if (this.canClose) this.close();
+		},
+
+		onBodyMousedown() {
+			this.top();
+		},
+
+		onHeaderMousedown(e) {
+			const main = this.$refs.main as any;
+
+			if (!contains(main, document.activeElement)) main.focus();
+
+			const position = main.getBoundingClientRect();
+
+			const clickX = e.clientX;
+			const clickY = e.clientY;
+			const moveBaseX = clickX - position.left;
+			const moveBaseY = clickY - position.top;
+			const browserWidth = window.innerWidth;
+			const browserHeight = window.innerHeight;
+			const windowWidth = main.offsetWidth;
+			const windowHeight = main.offsetHeight;
+
+			// 動かした時
+			dragListen(me => {
+				let moveLeft = me.clientX - moveBaseX;
+				let moveTop = me.clientY - moveBaseY;
+
+				// 上はみ出し
+				if (moveTop < 0) moveTop = 0;
+
+				// 左はみ出し
+				if (moveLeft < 0) moveLeft = 0;
+
+				// 下はみ出し
+				if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight;
+
+				// 右はみ出し
+				if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
+
+				main.style.left = moveLeft + 'px';
+				main.style.top = moveTop + 'px';
+			});
+		},
+
+		// 上ハンドル掴み時
+		onTopHandleMousedown(e) {
+			const main = this.$refs.main as any;
+
+			const base = e.clientY;
+			const height = parseInt(getComputedStyle(main, '').height, 10);
+			const top = parseInt(getComputedStyle(main, '').top, 10);
+
+			// 動かした時
+			dragListen(me => {
+				const move = me.clientY - base;
+				if (top + move > 0) {
+					if (height + -move > minHeight) {
+						this.applyTransformHeight(height + -move);
+						this.applyTransformTop(top + move);
+					} else { // 最小の高さより小さくなろうとした時
+						this.applyTransformHeight(minHeight);
+						this.applyTransformTop(top + (height - minHeight));
+					}
+				} else { // 上のはみ出し時
+					this.applyTransformHeight(top + height);
+					this.applyTransformTop(0);
+				}
+			});
+		},
+
+		// 右ハンドル掴み時
+		onRightHandleMousedown(e) {
+			const main = this.$refs.main as any;
+
+			const base = e.clientX;
+			const width = parseInt(getComputedStyle(main, '').width, 10);
+			const left = parseInt(getComputedStyle(main, '').left, 10);
+			const browserWidth = window.innerWidth;
+
+			// 動かした時
+			dragListen(me => {
+				const move = me.clientX - base;
+				if (left + width + move < browserWidth) {
+					if (width + move > minWidth) {
+						this.applyTransformWidth(width + move);
+					} else { // 最小の幅より小さくなろうとした時
+						this.applyTransformWidth(minWidth);
+					}
+				} else { // 右のはみ出し時
+					this.applyTransformWidth(browserWidth - left);
+				}
+			});
+		},
+
+		// 下ハンドル掴み時
+		onBottomHandleMousedown(e) {
+			const main = this.$refs.main as any;
+
+			const base = e.clientY;
+			const height = parseInt(getComputedStyle(main, '').height, 10);
+			const top = parseInt(getComputedStyle(main, '').top, 10);
+			const browserHeight = window.innerHeight;
+
+			// 動かした時
+			dragListen(me => {
+				const move = me.clientY - base;
+				if (top + height + move < browserHeight) {
+					if (height + move > minHeight) {
+						this.applyTransformHeight(height + move);
+					} else { // 最小の高さより小さくなろうとした時
+						this.applyTransformHeight(minHeight);
+					}
+				} else { // 下のはみ出し時
+					this.applyTransformHeight(browserHeight - top);
+				}
+			});
+		},
+
+		// 左ハンドル掴み時
+		onLeftHandleMousedown(e) {
+			const main = this.$refs.main as any;
+
+			const base = e.clientX;
+			const width = parseInt(getComputedStyle(main, '').width, 10);
+			const left = parseInt(getComputedStyle(main, '').left, 10);
+
+			// 動かした時
+			dragListen(me => {
+				const move = me.clientX - base;
+				if (left + move > 0) {
+					if (width + -move > minWidth) {
+						this.applyTransformWidth(width + -move);
+						this.applyTransformLeft(left + move);
+					} else { // 最小の幅より小さくなろうとした時
+						this.applyTransformWidth(minWidth);
+						this.applyTransformLeft(left + (width - minWidth));
+					}
+				} else { // 左のはみ出し時
+					this.applyTransformWidth(left + width);
+					this.applyTransformLeft(0);
+				}
+			});
+		},
+
+		// 左上ハンドル掴み時
+		onTopLeftHandleMousedown(e) {
+			this.onTopHandleMousedown(e);
+			this.onLeftHandleMousedown(e);
+		},
+
+		// 右上ハンドル掴み時
+		onTopRightHandleMousedown(e) {
+			this.onTopHandleMousedown(e);
+			this.onRightHandleMousedown(e);
+		},
+
+		// 右下ハンドル掴み時
+		onBottomRightHandleMousedown(e) {
+			this.onBottomHandleMousedown(e);
+			this.onRightHandleMousedown(e);
+		},
+
+		// 左下ハンドル掴み時
+		onBottomLeftHandleMousedown(e) {
+			this.onBottomHandleMousedown(e);
+			this.onLeftHandleMousedown(e);
+		},
+
+		// 高さを適用
+		applyTransformHeight(height) {
+			(this.$refs.main as any).style.height = height + 'px';
+		},
+
+		// 幅を適用
+		applyTransformWidth(width) {
+			(this.$refs.main as any).style.width = width + 'px';
+		},
+
+		// Y座標を適用
+		applyTransformTop(top) {
+			(this.$refs.main as any).style.top = top + 'px';
+		},
+
+		// X座標を適用
+		applyTransformLeft(left) {
+			(this.$refs.main as any).style.left = left + 'px';
+		},
+
+		onDragover(e) {
+			e.dataTransfer.dropEffect = 'none';
+		},
+
+		onKeydown(e) {
+			if (e.which == 27) { // Esc
+				if (this.canClose) {
+					e.preventDefault();
+					e.stopPropagation();
+					this.close();
+				}
+			}
+		},
+
+		onBrowserResize() {
+			const main = this.$refs.main as any;
+			const position = main.getBoundingClientRect();
+			const browserWidth = window.innerWidth;
+			const browserHeight = window.innerHeight;
+			const windowWidth = main.offsetWidth;
+			const windowHeight = main.offsetHeight;
+			if (position.left < 0) main.style.left = 0;
+			if (position.top < 0) main.style.top = 0;
+			if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px';
+			if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px';
 		}
 	}
-};
-
+});
 </script>
 
-
 <style lang="stylus" scoped>
 .mk-window
 	display block
@@ -584,4 +584,3 @@ this.onKeydown = e => {
 			height calc(100% - 40px)
 
 </style>
-

From ea2b5a5aace235ce7355644c65ee831dd83e551a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 23:26:35 +0900
Subject: [PATCH 044/286] wip

---
 src/web/app/common/scripts/text-compiler.ts   | 48 ---------
 src/web/app/common/views/components/index.ts  |  2 +
 .../app/common/views/components/post-html.ts  | 98 +++++++++++++++++++
 .../views/components/timeline-post.vue        | 10 +-
 .../app/desktop/views/components/timeline.vue |  4 +-
 .../app/desktop/views/components/window.vue   |  2 +-
 6 files changed, 105 insertions(+), 59 deletions(-)
 delete mode 100644 src/web/app/common/scripts/text-compiler.ts
 create mode 100644 src/web/app/common/views/components/post-html.ts

diff --git a/src/web/app/common/scripts/text-compiler.ts b/src/web/app/common/scripts/text-compiler.ts
deleted file mode 100644
index e0ea47df2..000000000
--- a/src/web/app/common/scripts/text-compiler.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-declare const _URL_: string;
-
-import * as riot from 'riot';
-import * as pictograph from 'pictograph';
-
-const escape = text =>
-	text
-		.replace(/>/g, '&gt;')
-		.replace(/</g, '&lt;');
-
-export default (tokens, shouldBreak) => {
-	if (shouldBreak == null) {
-		shouldBreak = true;
-	}
-
-	const me = (riot as any).mixin('i').me;
-
-	let text = tokens.map(token => {
-		switch (token.type) {
-			case 'text':
-				return escape(token.content)
-					.replace(/(\r\n|\n|\r)/g, shouldBreak ? '<br>' : ' ');
-			case 'bold':
-				return `<strong>${escape(token.bold)}</strong>`;
-			case 'url':
-				return `<mk-url href="${escape(token.content)}" target="_blank"></mk-url>`;
-			case 'link':
-				return `<a class="link" href="${escape(token.url)}" target="_blank" title="${escape(token.url)}">${escape(token.title)}</a>`;
-			case 'mention':
-				return `<a href="${_URL_ + '/' + escape(token.username)}" target="_blank" data-user-preview="${token.content}" ${me && me.username == token.username ? 'data-is-me' : ''}>${token.content}</a>`;
-			case 'hashtag': // TODO
-				return `<a>${escape(token.content)}</a>`;
-			case 'code':
-				return `<pre><code>${token.html}</code></pre>`;
-			case 'inline-code':
-				return `<code>${token.html}</code>`;
-			case 'emoji':
-				return pictograph.dic[token.emoji] || token.content;
-		}
-	}).join('');
-
-	// Remove needless whitespaces
-	text = text
-		.replace(/ <code>/g, '<code>').replace(/<\/code> /g, '</code>')
-		.replace(/<br><code><pre>/g, '<code><pre>').replace(/<\/code><\/pre><br>/g, '</code></pre>');
-
-	return text;
-};
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 9097c3081..c4c3475ee 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -4,8 +4,10 @@ import signin from './signin.vue';
 import signup from './signup.vue';
 import forkit from './forkit.vue';
 import nav from './nav.vue';
+import postHtml from './post-html';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
 Vue.component('mk-forkit', forkit);
 Vue.component('mk-nav', nav);
+Vue.component('mk-post-html', postHtml);
diff --git a/src/web/app/common/views/components/post-html.ts b/src/web/app/common/views/components/post-html.ts
new file mode 100644
index 000000000..88ced0342
--- /dev/null
+++ b/src/web/app/common/views/components/post-html.ts
@@ -0,0 +1,98 @@
+declare const _URL_: string;
+
+import Vue from 'vue';
+import * as pictograph from 'pictograph';
+
+import MkUrl from './url.vue';
+
+const escape = text =>
+	text
+		.replace(/>/g, '&gt;')
+		.replace(/</g, '&lt;');
+
+export default Vue.component('mk-post-html', {
+	props: {
+		ast: {
+			type: Array,
+			required: true
+		},
+		shouldBreak: {
+			type: Boolean,
+			default: true
+		},
+		i: {
+			type: Object,
+			default: null
+		}
+	},
+	render(createElement) {
+		const els = [].concat.apply([], (this as any).ast.map(token => {
+			switch (token.type) {
+				case 'text':
+					const text = escape(token.content)
+						.replace(/(\r\n|\n|\r)/g, '\n');
+
+					if ((this as any).shouldBreak) {
+						return text.split('\n').map(t => [createElement('span', t), createElement('br')]);
+					} else {
+						return createElement('span', text.replace(/\n/g, ' '));
+					}
+
+				case 'bold':
+					return createElement('strong', escape(token.bold));
+
+				case 'url':
+					return createElement(MkUrl, {
+						props: {
+							href: escape(token.content),
+							target: '_blank'
+						}
+					});
+
+				case 'link':
+					return createElement('a', {
+						attrs: {
+							class: 'link',
+							href: escape(token.url),
+							target: '_blank',
+							title: escape(token.url)
+						}
+					}, escape(token.title));
+
+				case 'mention':
+					return (createElement as any)('a', {
+						attrs: {
+							href: `${_URL_}/${escape(token.username)}`,
+							target: '_blank',
+							dataIsMe: (this as any).i && (this as any).i.username == token.username
+						},
+						directives: [{
+							name: 'user-preview',
+							value: token.content
+						}]
+					}, token.content);
+
+				case 'hashtag':
+					return createElement('a', {
+						attrs: {
+							href: `${_URL_}/search?q=${escape(token.content)}`,
+							target: '_blank'
+						}
+					}, escape(token.content));
+
+				case 'code':
+					return createElement('pre', [
+						createElement('code', token.html)
+					]);
+
+				case 'inline-code':
+					return createElement('code', token.html);
+
+				case 'emoji':
+					return createElement('span', pictograph.dic[token.emoji] || token.content);
+			}
+		}));
+
+		return createElement('div', els);
+	}
+});
diff --git a/src/web/app/desktop/views/components/timeline-post.vue b/src/web/app/desktop/views/components/timeline-post.vue
index e4eaa8f79..f722ea334 100644
--- a/src/web/app/desktop/views/components/timeline-post.vue
+++ b/src/web/app/desktop/views/components/timeline-post.vue
@@ -32,7 +32,7 @@
 				<div class="text" ref="text">
 					<p class="channel" v-if="p.channel"><a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:</p>
 					<a class="reply" v-if="p.reply">%fa:reply%</a>
-					<p class="dummy"></p>
+					<mk-post-html :ast="p.ast" :i="$root.$data.os.i"/>
 					<a class="quote" v-if="p.repost">RP:</a>
 				</div>
 				<div class="media" v-if="p.media">
@@ -94,7 +94,7 @@ export default Vue.extend({
 			return this.isRepost ? this.post.repost : this.post;
 		},
 		reactionsCount(): number {
-			return this.p.reaction_counts ? Object.keys(this.p.reaction_counts).map(key => this.p.reaction_counts[key]).reduce((a, b) => a + b) : 0;			
+			return this.p.reaction_counts ? Object.keys(this.p.reaction_counts).map(key => this.p.reaction_counts[key]).reduce((a, b) => a + b) : 0;
 		},
 		title(): string {
 			return dateStringify(this.p.created_at);
@@ -117,12 +117,6 @@ export default Vue.extend({
 		if (this.p.text) {
 			const tokens = this.p.ast;
 
-			this.$refs.text.innerHTML = this.$refs.text.innerHTML.replace('<p class="dummy"></p>', compile(tokens));
-
-			Array.from(this.$refs.text.children).forEach(e => {
-				if (e.tagName == 'MK-URL') riot.mount(e);
-			});
-
 			// URLをプレビュー
 			tokens
 			.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index ba412848f..161eebdf7 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -7,7 +7,7 @@
 	<footer data-yield="footer">
 		<yield from="footer"/>
 	</footer>
-</div>	
+</div>
 </template>
 
 <script lang="ts">
@@ -31,7 +31,7 @@ export default Vue.extend({
 	},
 	methods: {
 		focus() {
-			this.$refs.root.children[0].focus();
+			(this.$refs.root as any).children[0].focus();
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index ac3af3a57..28f368253 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -27,7 +27,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import anime from 'animejs';
-import contains from '../../common/scripts/contains';
+import contains from '../../../common/scripts/contains';
 
 const minHeight = 40;
 const minWidth = 200;

From cbec4229d3648a934ef9dbe23efdd1c8f91ade27 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 11 Feb 2018 23:35:32 +0900
Subject: [PATCH 045/286] wip

---
 .../common/views/components/url-preview.vue   | 199 +++++++++---------
 .../views/components/timeline-post.vue        |  23 +-
 2 files changed, 108 insertions(+), 114 deletions(-)

diff --git a/src/web/app/common/views/components/url-preview.vue b/src/web/app/common/views/components/url-preview.vue
index 88158db84..b84634617 100644
--- a/src/web/app/common/views/components/url-preview.vue
+++ b/src/web/app/common/views/components/url-preview.vue
@@ -1,126 +1,123 @@
 <template>
-	<a :href="url" target="_blank" :title="url" v-if="!fetching">
-		<div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div>
-		<article>
-			<header>
-				<h1>{{ title }}</h1>
-			</header>
-			<p>{{ description }}</p>
-			<footer>
-				<img class="icon" v-if="icon" :src="icon"/>
-				<p>{{ sitename }}</p>
-			</footer>
-		</article>
-	</a>
+<a class="mk-url-preview" :href="url" target="_blank" :title="url" v-if="!fetching">
+	<div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div>
+	<article>
+		<header>
+			<h1>{{ title }}</h1>
+		</header>
+		<p>{{ description }}</p>
+		<footer>
+			<img class="icon" v-if="icon" :src="icon"/>
+			<p>{{ sitename }}</p>
+		</footer>
+	</article>
+</a>
 </template>
 
-<script lang="typescript">
-	export default {
-		props: ['url'],
-		data() {
-			return {
-				fetching: true,
-				title: null,
-				description: null,
-				thumbnail: null,
-				icon: null,
-				sitename: null
-			};
-		},
-		created() {
-			fetch('/api:url?url=' + this.url).then(res => {
-				res.json().then(info => {
-					this.title = info.title;
-					this.description = info.description;
-					this.thumbnail = info.thumbnail;
-					this.icon = info.icon;
-					this.sitename = info.sitename;
+<script lang="ts">
+import Vue from 'vue';
 
-					this.fetching = false;
-				});
+export default Vue.extend({
+	props: ['url'],
+	data() {
+		return {
+			fetching: true,
+			title: null,
+			description: null,
+			thumbnail: null,
+			icon: null,
+			sitename: null
+		};
+	},
+	created() {
+		fetch('/api:url?url=' + this.url).then(res => {
+			res.json().then(info => {
+				this.title = info.title;
+				this.description = info.description;
+				this.thumbnail = info.thumbnail;
+				this.icon = info.icon;
+				this.sitename = info.sitename;
+
+				this.fetching = false;
 			});
-		}
-	};
+		});
+	}
+});
 </script>
 
 <style lang="stylus" scoped>
-	:scope
-		display block
-		font-size 16px
+.mk-url-preview
+	display block
+	font-size 16px
+	border solid 1px #eee
+	border-radius 4px
+	overflow hidden
 
-		> a
-			display block
-			border solid 1px #eee
-			border-radius 4px
-			overflow hidden
+	&:hover
+		text-decoration none
+		border-color #ddd
 
-			&:hover
-				text-decoration none
-				border-color #ddd
+		> article > header > h1
+			text-decoration underline
 
-				> article > header > h1
-					text-decoration underline
+	> .thumbnail
+		position absolute
+		width 100px
+		height 100%
+		background-position center
+		background-size cover
 
-			> .thumbnail
-				position absolute
-				width 100px
-				height 100%
-				background-position center
-				background-size cover
+		& + article
+			left 100px
+			width calc(100% - 100px)
 
-				& + article
-					left 100px
-					width calc(100% - 100px)
+	> article
+		padding 16px
 
-			> article
-				padding 16px
+		> header
+			margin-bottom 8px
 
-				> header
-					margin-bottom 8px
+			> h1
+				margin 0
+				font-size 1em
+				color #555
 
-					> h1
-						margin 0
-						font-size 1em
-						color #555
+		> p
+			margin 0
+			color #777
+			font-size 0.8em
 
-				> p
-					margin 0
-					color #777
-					font-size 0.8em
+		> footer
+			margin-top 8px
+			height 16px
 
-				> footer
-					margin-top 8px
-					height 16px
+			> img
+				display inline-block
+				width 16px
+				height 16px
+				margin-right 4px
+				vertical-align top
 
-					> img
-						display inline-block
-						width 16px
-						height 16px
-						margin-right 4px
-						vertical-align top
+			> p
+				display inline-block
+				margin 0
+				color #666
+				font-size 0.8em
+				line-height 16px
+				vertical-align top
 
-					> p
-						display inline-block
-						margin 0
-						color #666
-						font-size 0.8em
-						line-height 16px
-						vertical-align top
+	@media (max-width 500px)
+		font-size 8px
+		border none
 
-		@media (max-width 500px)
-			font-size 8px
+		> .thumbnail
+			width 70px
 
-			> a
-				border none
+			& + article
+				left 70px
+				width calc(100% - 70px)
 
-				> .thumbnail
-					width 70px
-
-					& + article
-						left 70px
-						width calc(100% - 70px)
-
-				> article
-					padding 8px
+		> article
+			padding 8px
 
 </style>
diff --git a/src/web/app/desktop/views/components/timeline-post.vue b/src/web/app/desktop/views/components/timeline-post.vue
index f722ea334..ed0596741 100644
--- a/src/web/app/desktop/views/components/timeline-post.vue
+++ b/src/web/app/desktop/views/components/timeline-post.vue
@@ -34,6 +34,7 @@
 					<a class="reply" v-if="p.reply">%fa:reply%</a>
 					<mk-post-html :ast="p.ast" :i="$root.$data.os.i"/>
 					<a class="quote" v-if="p.repost">RP:</a>
+					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 				</div>
 				<div class="media" v-if="p.media">
 					<mk-images :images="p.media"/>
@@ -101,6 +102,15 @@ export default Vue.extend({
 		},
 		url(): string {
 			return `/${this.p.user.username}/${this.p.id}`;
+		},
+		urls(): string[] {
+			if (this.p.ast) {
+				return this.p.ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
 		}
 	},
 	created() {
@@ -113,19 +123,6 @@ export default Vue.extend({
 		if (this.$root.$data.os.isSignedIn) {
 			this.connection.on('_connected_', this.onStreamConnected);
 		}
-
-		if (this.p.text) {
-			const tokens = this.p.ast;
-
-			// URLをプレビュー
-			tokens
-			.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
-			.map(t => {
-				riot.mount(this.$refs.text.appendChild(document.createElement('mk-url-preview')), {
-					url: t.url
-				});
-			});
-		}
 	},
 	beforeDestroy() {
 		this.decapture(true);

From eb1fa6e43c43baf871aa7a2fd5311ac997523923 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 00:17:51 +0900
Subject: [PATCH 046/286] wip

---
 src/web/app/common/views/directives/focus.ts  |  5 ++
 src/web/app/common/views/directives/index.ts  |  5 ++
 .../app/desktop/-tags/post-form-window.tag    | 68 -------------------
 .../views/components/post-form-window.vue     | 63 +++++++++++++++++
 .../views/components/timeline-post.vue        | 16 ++---
 .../app/desktop/views/components/window.vue   |  4 +-
 src/web/app/init.ts                           |  9 ++-
 7 files changed, 89 insertions(+), 81 deletions(-)
 create mode 100644 src/web/app/common/views/directives/focus.ts
 create mode 100644 src/web/app/common/views/directives/index.ts
 delete mode 100644 src/web/app/desktop/-tags/post-form-window.tag
 create mode 100644 src/web/app/desktop/views/components/post-form-window.vue

diff --git a/src/web/app/common/views/directives/focus.ts b/src/web/app/common/views/directives/focus.ts
new file mode 100644
index 000000000..b4fbcb6a8
--- /dev/null
+++ b/src/web/app/common/views/directives/focus.ts
@@ -0,0 +1,5 @@
+export default {
+	inserted(el) {
+		el.focus();
+	}
+};
diff --git a/src/web/app/common/views/directives/index.ts b/src/web/app/common/views/directives/index.ts
new file mode 100644
index 000000000..358866f50
--- /dev/null
+++ b/src/web/app/common/views/directives/index.ts
@@ -0,0 +1,5 @@
+import Vue from 'vue';
+
+import focus from './focus';
+
+Vue.directive('focus', focus);
diff --git a/src/web/app/desktop/-tags/post-form-window.tag b/src/web/app/desktop/-tags/post-form-window.tag
deleted file mode 100644
index 562621bde..000000000
--- a/src/web/app/desktop/-tags/post-form-window.tag
+++ /dev/null
@@ -1,68 +0,0 @@
-<mk-post-form-window>
-	<mk-window ref="window" is-modal={ true }>
-		<yield to="header">
-			<span v-if="!parent.opts.reply">%i18n:desktop.tags.mk-post-form-window.post%</span>
-			<span v-if="parent.opts.reply">%i18n:desktop.tags.mk-post-form-window.reply%</span>
-			<span class="files" v-if="parent.files.length != 0">{ '%i18n:desktop.tags.mk-post-form-window.attaches%'.replace('{}', parent.files.length) }</span>
-			<span class="uploading-files" v-if="parent.uploadingFiles.length != 0">{ '%i18n:desktop.tags.mk-post-form-window.uploading-media%'.replace('{}', parent.uploadingFiles.length) }<mk-ellipsis/></span>
-		</yield>
-		<yield to="content">
-			<div class="ref" v-if="parent.opts.reply">
-				<mk-post-preview post={ parent.opts.reply }/>
-			</div>
-			<div class="body">
-				<mk-post-form ref="form" reply={ parent.opts.reply }/>
-			</div>
-		</yield>
-	</mk-window>
-	<style lang="stylus" scoped>
-		:scope
-			> mk-window
-
-				[data-yield='header']
-					> .files
-					> .uploading-files
-						margin-left 8px
-						opacity 0.8
-
-						&:before
-							content '('
-
-						&:after
-							content ')'
-
-				[data-yield='content']
-					> .ref
-						> mk-post-preview
-							margin 16px 22px
-
-	</style>
-	<script lang="typescript">
-		this.uploadingFiles = [];
-		this.files = [];
-
-		this.on('mount', () => {
-			this.$refs.window.refs.form.focus();
-
-			this.$refs.window.on('closed', () => {
-				this.$destroy();
-			});
-
-			this.$refs.window.refs.form.on('post', () => {
-				this.$refs.window.close();
-			});
-
-			this.$refs.window.refs.form.on('change-uploading-files', files => {
-				this.update({
-					uploadingFiles: files || []
-				});
-			});
-
-			this.$refs.window.refs.form.on('change-files', files => {
-				this.update({
-					files: files || []
-				});
-			});
-		});
-	</script>
-</mk-post-form-window>
diff --git a/src/web/app/desktop/views/components/post-form-window.vue b/src/web/app/desktop/views/components/post-form-window.vue
new file mode 100644
index 000000000..37670ccd9
--- /dev/null
+++ b/src/web/app/desktop/views/components/post-form-window.vue
@@ -0,0 +1,63 @@
+<template>
+<mk-window ref="window" is-modal @closed="$destroy">
+	<span slot="header">
+		<span v-if="!parent.opts.reply">%i18n:desktop.tags.mk-post-form-window.post%</span>
+		<span v-if="parent.opts.reply">%i18n:desktop.tags.mk-post-form-window.reply%</span>
+		<span :class="$style.files" v-if="parent.files.length != 0">{ '%i18n:desktop.tags.mk-post-form-window.attaches%'.replace('{}', parent.files.length) }</span>
+		<span :class="$style.files" v-if="parent.uploadingFiles.length != 0">{ '%i18n:desktop.tags.mk-post-form-window.uploading-media%'.replace('{}', parent.uploadingFiles.length) }<mk-ellipsis/></span>
+	</span>
+	<div slot="content">
+		<div class="ref" v-if="parent.opts.reply">
+			<mk-post-preview :class="$style.postPreview" :post="reply"/>
+		</div>
+		<div class="body">
+			<mk-post-form ref="form"
+				:reply="reply"
+				@post="$refs.window.close"
+				@change-uploadings="onChangeUploadings"
+				@change-attached-media="onChangeMedia"/>
+		</div>
+	</div>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['reply'],
+	data() {
+		return {
+			uploadings: [],
+			media: []
+		};
+	},
+	mounted() {
+		(this.$refs.form as any).focus();
+	},
+	methods: {
+		onChangeUploadings(media) {
+			this.uploadings = media;
+		},
+		onChangeMedia(media) {
+			this.media = media;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.files
+	margin-left 8px
+	opacity 0.8
+
+	&:before
+		content '('
+
+	&:after
+		content ')'
+
+.postPreview
+	margin 16px 22px
+
+</style>
diff --git a/src/web/app/desktop/views/components/timeline-post.vue b/src/web/app/desktop/views/components/timeline-post.vue
index ed0596741..38f5f0891 100644
--- a/src/web/app/desktop/views/components/timeline-post.vue
+++ b/src/web/app/desktop/views/components/timeline-post.vue
@@ -73,8 +73,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import compile from '../../common/scripts/text-compiler';
-import dateStringify from '../../common/scripts/date-stringify';
+import dateStringify from '../../../common/scripts/date-stringify';
 
 export default Vue.extend({
 	props: ['post'],
@@ -156,6 +155,13 @@ export default Vue.extend({
 			if (post.id == this.post.id) {
 				this.$emit('update:post', post);
 			}
+		},
+		reply() {
+			document.body.appendChild(new MkPostFormWindow({
+				propsData: {
+					reply: this.p
+				}
+			}).$mount().$el);
 		}
 	}
 });
@@ -163,12 +169,6 @@ export default Vue.extend({
 
 <script lang="typescript">
 
-this.reply = () => {
-	riot.mount(document.body.appendChild(document.createElement('mk-post-form-window')), {
-		reply: this.p
-	});
-};
-
 this.repost = () => {
 	riot.mount(document.body.appendChild(document.createElement('mk-repost-form-window')), {
 		post: this.p
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 28f368253..26f3cbcd3 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -4,13 +4,13 @@
 	<div class="main" ref="main" tabindex="-1" :data-is-modal="isModal" @mousedown="onBodyMousedown" @keydown="onKeydown" :style="{ width, height }">
 		<div class="body">
 			<header ref="header" @contextmenu.prevent="() => {}" @mousedown.prevent="onHeaderMousedown">
-				<h1 data-yield="header"><yield from="header"/></h1>
+				<h1><slot name="header"></slot></h1>
 				<div>
 					<button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="ポップアウト">%fa:R window-restore%</button>
 					<button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="閉じる">%fa:times%</button>
 				</div>
 			</header>
-			<div class="content" data-yield="content"><yield from="content"/></div>
+			<div class="content"><slot name="content"></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>
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index dfb1e96b8..4ef2a8921 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -14,6 +14,12 @@ import VModal from 'vue-js-modal';
 Vue.use(VueRouter);
 Vue.use(VModal);
 
+// Register global directives
+require('./common/views/directives');
+
+// Register global components
+require('./common/views/components');
+
 import App from './app.vue';
 
 import checkForUpdate from './common/scripts/check-for-update';
@@ -70,9 +76,6 @@ export default (callback: (launch: () => Vue) => void, sw = false) => {
 		// アプリ基底要素マウント
 		document.body.innerHTML = '<div id="app"></div>';
 
-		// Register global components
-		require('./common/views/components');
-
 		const launch = () => {
 			return new Vue({
 				data: {

From 5f8021dfff90d3a36e00b707bfbb281cfb6c3766 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 00:23:05 +0900
Subject: [PATCH 047/286] wip

---
 .../views/components/post-form-window.vue     | 24 ++++++++-----------
 1 file changed, 10 insertions(+), 14 deletions(-)

diff --git a/src/web/app/desktop/views/components/post-form-window.vue b/src/web/app/desktop/views/components/post-form-window.vue
index 37670ccd9..f488b6c34 100644
--- a/src/web/app/desktop/views/components/post-form-window.vue
+++ b/src/web/app/desktop/views/components/post-form-window.vue
@@ -1,22 +1,18 @@
 <template>
-<mk-window ref="window" is-modal @closed="$destroy">
+<mk-window is-modal @closed="$destroy">
 	<span slot="header">
 		<span v-if="!parent.opts.reply">%i18n:desktop.tags.mk-post-form-window.post%</span>
 		<span v-if="parent.opts.reply">%i18n:desktop.tags.mk-post-form-window.reply%</span>
-		<span :class="$style.files" v-if="parent.files.length != 0">{ '%i18n:desktop.tags.mk-post-form-window.attaches%'.replace('{}', parent.files.length) }</span>
-		<span :class="$style.files" v-if="parent.uploadingFiles.length != 0">{ '%i18n:desktop.tags.mk-post-form-window.uploading-media%'.replace('{}', parent.uploadingFiles.length) }<mk-ellipsis/></span>
+		<span :class="$style.count" v-if="media.length != 0">{{ '%i18n:desktop.tags.mk-post-form-window.attaches%'.replace('{}', media.length) }}</span>
+		<span :class="$style.count" v-if="uploadings.length != 0">{{ '%i18n:desktop.tags.mk-post-form-window.uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
 	</span>
 	<div slot="content">
-		<div class="ref" v-if="parent.opts.reply">
-			<mk-post-preview :class="$style.postPreview" :post="reply"/>
-		</div>
-		<div class="body">
-			<mk-post-form ref="form"
-				:reply="reply"
-				@post="$refs.window.close"
-				@change-uploadings="onChangeUploadings"
-				@change-attached-media="onChangeMedia"/>
-		</div>
+		<mk-post-preview v-if="parent.opts.reply" :class="$style.postPreview" :post="reply"/>
+		<mk-post-form ref="form"
+			:reply="reply"
+			@post="$refs.window.close"
+			@change-uploadings="onChangeUploadings"
+			@change-attached-media="onChangeMedia"/>
 	</div>
 </mk-window>
 </template>
@@ -47,7 +43,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" module>
-.files
+.count
 	margin-left 8px
 	opacity 0.8
 

From 249fdc1b5d0c3f6eb2f643e2debf310b2b2bf4df Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 00:29:10 +0900
Subject: [PATCH 048/286] wip

---
 src/web/app/common/views/components/url.vue | 101 ++++++++++----------
 1 file changed, 51 insertions(+), 50 deletions(-)

diff --git a/src/web/app/common/views/components/url.vue b/src/web/app/common/views/components/url.vue
index 4cc76f7e2..14d4fc82f 100644
--- a/src/web/app/common/views/components/url.vue
+++ b/src/web/app/common/views/components/url.vue
@@ -1,65 +1,66 @@
 <template>
-	<a :href="url" :target="target">
-		<span class="schema">{{ schema }}//</span>
-		<span class="hostname">{{ hostname }}</span>
-		<span class="port" v-if="port != ''">:{{ port }}</span>
-		<span class="pathname" v-if="pathname != ''">{{ pathname }}</span>
-		<span class="query">{{ query }}</span>
-		<span class="hash">{{ hash }}</span>
-		%fa:external-link-square-alt%
-	</a>
+<a class="mk-url" :href="url" :target="target">
+	<span class="schema">{{ schema }}//</span>
+	<span class="hostname">{{ hostname }}</span>
+	<span class="port" v-if="port != ''">:{{ port }}</span>
+	<span class="pathname" v-if="pathname != ''">{{ pathname }}</span>
+	<span class="query">{{ query }}</span>
+	<span class="hash">{{ hash }}</span>
+	%fa:external-link-square-alt%
+</a>
 </template>
 
-<script lang="typescript">
-	export default {
-		props: ['url', 'target'],
-		data() {
-			return {
-				schema: null,
-				hostname: null,
-				port: null,
-				pathname: null,
-				query: null,
-				hash: null
-			};
-		},
-		created() {
-			const url = new URL(this.url);
+<script lang="ts">
+import Vue from 'vue';
 
-			this.schema = url.protocol;
-			this.hostname = url.hostname;
-			this.port = url.port;
-			this.pathname = url.pathname;
-			this.query = url.search;
-			this.hash = url.hash;
-		}
-	};
+export default Vue.extend({
+	props: ['url', 'target'],
+	data() {
+		return {
+			schema: null,
+			hostname: null,
+			port: null,
+			pathname: null,
+			query: null,
+			hash: null
+		};
+	},
+	created() {
+		const url = new URL(this.url);
+
+		this.schema = url.protocol;
+		this.hostname = url.hostname;
+		this.port = url.port;
+		this.pathname = url.pathname;
+		this.query = url.search;
+		this.hash = url.hash;
+	}
+});
 </script>
 
 <style lang="stylus" scoped>
-	:scope
-		word-break break-all
+.mk-url
+	word-break break-all
 
-	> a
-		> [data-fa]
-			padding-left 2px
-			font-size .9em
-			font-weight 400
-			font-style normal
+	> [data-fa]
+		padding-left 2px
+		font-size .9em
+		font-weight 400
+		font-style normal
 
-		> .schema
-			opacity 0.5
+	> .schema
+		opacity 0.5
 
-		> .hostname
-			font-weight bold
+	> .hostname
+		font-weight bold
 
-		> .pathname
-			opacity 0.8
+	> .pathname
+		opacity 0.8
 
-		> .query
-			opacity 0.5
+	> .query
+		opacity 0.5
 
-		> .hash
-			font-style italic
+	> .hash
+		font-style italic
 
 </style>

From 910edf7c5fe4fc585ca99efcdb9ea7bf19339859 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 00:41:48 +0900
Subject: [PATCH 049/286] wip

---
 src/web/app/desktop/views/components/index.ts   | 12 ++++++++++++
 .../views/components/timeline-post-sub.vue      | 17 +++++++++++------
 .../desktop/views/components/timeline-post.vue  | 14 +++++++++-----
 src/web/app/desktop/views/components/window.vue |  2 +-
 4 files changed, 33 insertions(+), 12 deletions(-)

diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 8c490ef6d..b2de82b4d 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -2,6 +2,18 @@ import Vue from 'vue';
 
 import ui from './ui.vue';
 import home from './home.vue';
+import timeline from './timeline.vue';
+import timelinePost from './timeline-post.vue';
+import timelinePostSub from './timeline-post-sub.vue';
+import subPostContent from './sub-post-content.vue';
+import window from './window.vue';
+import postFormWindow from './post-form-window.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-home', home);
+Vue.component('mk-timeline', timeline);
+Vue.component('mk-timeline-post', timelinePost);
+Vue.component('mk-timeline-post-sub', timelinePostSub);
+Vue.component('mk-sub-post-content', subPostContent);
+Vue.component('mk-window', window);
+Vue.component('post-form-window', postFormWindow);
diff --git a/src/web/app/desktop/views/components/timeline-post-sub.vue b/src/web/app/desktop/views/components/timeline-post-sub.vue
index 27820901f..120939699 100644
--- a/src/web/app/desktop/views/components/timeline-post-sub.vue
+++ b/src/web/app/desktop/views/components/timeline-post-sub.vue
@@ -18,13 +18,18 @@
 </div>
 </template>
 
-<script lang="typescript">
-	import dateStringify from '../../common/scripts/date-stringify';
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../../common/scripts/date-stringify';
 
-	this.mixin('user-preview');
-
-	this.post = this.opts.post;
-	this.title = dateStringify(this.post.created_at);
+export default Vue.extend({
+	props: ['post'],
+	computed: {
+		title(): string {
+			return dateStringify(this.post.created_at);
+		}
+	}
+});
 </script>
 
 <style lang="stylus" scoped>
diff --git a/src/web/app/desktop/views/components/timeline-post.vue b/src/web/app/desktop/views/components/timeline-post.vue
index 38f5f0891..c18cff36a 100644
--- a/src/web/app/desktop/views/components/timeline-post.vue
+++ b/src/web/app/desktop/views/components/timeline-post.vue
@@ -74,6 +74,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
+import MkPostFormWindow from './post-form-window.vue';
+import MkRepostFormWindow from './repost-form-window.vue';
 
 export default Vue.extend({
 	props: ['post'],
@@ -162,6 +164,13 @@ export default Vue.extend({
 					reply: this.p
 				}
 			}).$mount().$el);
+		},
+		repost() {
+			document.body.appendChild(new MkRepostFormWindow({
+				propsData: {
+					post: this.p
+				}
+			}).$mount().$el);
 		}
 	}
 });
@@ -169,11 +178,6 @@ export default Vue.extend({
 
 <script lang="typescript">
 
-this.repost = () => {
-	riot.mount(document.body.appendChild(document.createElement('mk-repost-form-window')), {
-		post: this.p
-	});
-};
 
 this.react = () => {
 	riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), {
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 26f3cbcd3..986b151c4 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -63,7 +63,7 @@ export default Vue.extend({
 			default: 'auto'
 		},
 		popoutUrl: {
-			type: String
+			type: [String, Function]
 		}
 	},
 

From f494310c4fd2113996877e5dcba1e84c01d0cece Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 00:58:02 +0900
Subject: [PATCH 050/286] wip

---
 .../app/desktop/-tags/repost-form-window.tag  | 47 -------------------
 src/web/app/desktop/views/components/index.ts |  2 +
 .../views/components/post-form-window.vue     |  4 +-
 .../views/components/repost-form-window.vue   | 38 +++++++++++++++
 4 files changed, 42 insertions(+), 49 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/repost-form-window.tag
 create mode 100644 src/web/app/desktop/views/components/repost-form-window.vue

diff --git a/src/web/app/desktop/-tags/repost-form-window.tag b/src/web/app/desktop/-tags/repost-form-window.tag
deleted file mode 100644
index 25f509c62..000000000
--- a/src/web/app/desktop/-tags/repost-form-window.tag
+++ /dev/null
@@ -1,47 +0,0 @@
-<mk-repost-form-window>
-	<mk-window ref="window" is-modal={ true }>
-		<yield to="header">
-			%fa:retweet%%i18n:desktop.tags.mk-repost-form-window.title%
-		</yield>
-		<yield to="content">
-			<mk-repost-form ref="form" post={ parent.opts.post }/>
-		</yield>
-	</mk-window>
-	<style lang="stylus" scoped>
-		:scope
-			> mk-window
-				[data-yield='header']
-					> [data-fa]
-						margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.onDocumentKeydown = e => {
-			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
-				if (e.which == 27) { // Esc
-					this.$refs.window.close();
-				}
-			}
-		};
-
-		this.on('mount', () => {
-			this.$refs.window.refs.form.on('cancel', () => {
-				this.$refs.window.close();
-			});
-
-			this.$refs.window.refs.form.on('posted', () => {
-				this.$refs.window.close();
-			});
-
-			document.addEventListener('keydown', this.onDocumentKeydown);
-
-			this.$refs.window.on('closed', () => {
-				this.$destroy();
-			});
-		});
-
-		this.on('unmount', () => {
-			document.removeEventListener('keydown', this.onDocumentKeydown);
-		});
-	</script>
-</mk-repost-form-window>
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index b2de82b4d..9788a27f1 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -8,6 +8,7 @@ import timelinePostSub from './timeline-post-sub.vue';
 import subPostContent from './sub-post-content.vue';
 import window from './window.vue';
 import postFormWindow from './post-form-window.vue';
+import repostFormWindow from './repost-form-window.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-home', home);
@@ -17,3 +18,4 @@ Vue.component('mk-timeline-post-sub', timelinePostSub);
 Vue.component('mk-sub-post-content', subPostContent);
 Vue.component('mk-window', window);
 Vue.component('post-form-window', postFormWindow);
+Vue.component('repost-form-window', repostFormWindow);
diff --git a/src/web/app/desktop/views/components/post-form-window.vue b/src/web/app/desktop/views/components/post-form-window.vue
index f488b6c34..90e694c92 100644
--- a/src/web/app/desktop/views/components/post-form-window.vue
+++ b/src/web/app/desktop/views/components/post-form-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window is-modal @closed="$destroy">
+<mk-window ref="window" is-modal @closed="$destroy">
 	<span slot="header">
 		<span v-if="!parent.opts.reply">%i18n:desktop.tags.mk-post-form-window.post%</span>
 		<span v-if="parent.opts.reply">%i18n:desktop.tags.mk-post-form-window.reply%</span>
@@ -10,7 +10,7 @@
 		<mk-post-preview v-if="parent.opts.reply" :class="$style.postPreview" :post="reply"/>
 		<mk-post-form ref="form"
 			:reply="reply"
-			@post="$refs.window.close"
+			@posted="$refs.window.close"
 			@change-uploadings="onChangeUploadings"
 			@change-attached-media="onChangeMedia"/>
 	</div>
diff --git a/src/web/app/desktop/views/components/repost-form-window.vue b/src/web/app/desktop/views/components/repost-form-window.vue
new file mode 100644
index 000000000..6f06faaba
--- /dev/null
+++ b/src/web/app/desktop/views/components/repost-form-window.vue
@@ -0,0 +1,38 @@
+<template>
+<mk-window ref="window" is-modal @closed="$destroy">
+	<span slot="header" :class="$style.header">%fa:retweet%%i18n:desktop.tags.mk-repost-form-window.title%</span>
+	<div slot="content">
+		<mk-repost-form ref="form" :post="post" @posted="$refs.window.close" @canceled="$refs.window.close"/>
+	</div>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['post'],
+	mounted() {
+		document.addEventListener('keydown', this.onDocumentKeydown);
+	},
+	beforeDestroy() {
+		document.removeEventListener('keydown', this.onDocumentKeydown);
+	},
+	methods: {
+		onDocumentKeydown(e) {
+			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
+				if (e.which == 27) { // Esc
+					(this.$refs.window as any).close();
+				}
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.header
+	> [data-fa]
+		margin-right 4px
+
+</style>

From 6de528efca2cedb9cc2d69f96891fe0fdec85cf1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 01:06:17 +0900
Subject: [PATCH 051/286] wip

---
 .../desktop/views/components/post-form.vue    | 35 +++++++++++++++++++
 1 file changed, 35 insertions(+)
 create mode 100644 src/web/app/desktop/views/components/post-form.vue

diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
new file mode 100644
index 000000000..d021c9ab5
--- /dev/null
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -0,0 +1,35 @@
+<template>
+<div class="mk-post-form"
+	@dragover="onDragover"
+	@dragenter="onDragenter"
+	@dragleave="onDragleave"
+	@drop="onDrop"
+>
+	<div class="content">
+		<textarea :class="{ with: (files.length != 0 || poll) }" ref="text" :disabled="wait"
+			@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
+		></textarea>
+		<div class="medias" :class="{ with: poll }" v-show="files.length != 0">
+			<ul ref="media">
+				<li each={ files } data-id={ id }>
+					<div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name }></div>
+					<img class="remove" @click="removeFile" src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/>
+				</li>
+			</ul>
+			<p class="remain">{ 4 - files.length }/4</p>
+		</div>
+		<mk-poll-editor v-if="poll" ref="poll" ondestroy={ onPollDestroyed }/>
+	</div>
+	<mk-uploader ref="uploader"/>
+	<button ref="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" @click="selectFile">%fa:upload%</button>
+	<button ref="drive" title="%i18n:desktop.tags.mk-post-form.attach-media-from-drive%" @click="selectFileFromDrive">%fa:cloud%</button>
+	<button class="kao" title="%i18n:desktop.tags.mk-post-form.insert-a-kao%" @click="kao">%fa:R smile%</button>
+	<button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" @click="addPoll">%fa:chart-pie%</button>
+	<p class="text-count { over: refs.text.value.length > 1000 }">{ '%i18n:desktop.tags.mk-post-form.text-remain%'.replace('{}', 1000 - refs.text.value.length) }</p>
+	<button :class="{ wait: wait }" ref="submit" disabled={ wait || (refs.text.value.length == 0 && files.length == 0 && !poll && !repost) } @click="post">
+		{ wait ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }<mk-ellipsis v-if="wait"/>
+	</button>
+	<input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" onchange={ changeFile }/>
+	<div class="dropzone" v-if="draghover"></div>
+</div>
+</template>

From 4d19e8a1b2669a18c0af2ef24e8112e2b0ae6e8f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 09:06:22 +0900
Subject: [PATCH 052/286] wip

---
 src/web/app/desktop/-tags/images.tag          | 172 ------------------
 .../views/components/images-image-dialog.vue  |  69 +++++++
 .../desktop/views/components/images-image.vue |  66 +++++++
 .../app/desktop/views/components/images.vue   |  60 ++++++
 4 files changed, 195 insertions(+), 172 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/images.tag
 create mode 100644 src/web/app/desktop/views/components/images-image-dialog.vue
 create mode 100644 src/web/app/desktop/views/components/images-image.vue
 create mode 100644 src/web/app/desktop/views/components/images.vue

diff --git a/src/web/app/desktop/-tags/images.tag b/src/web/app/desktop/-tags/images.tag
deleted file mode 100644
index 1094e0d96..000000000
--- a/src/web/app/desktop/-tags/images.tag
+++ /dev/null
@@ -1,172 +0,0 @@
-<mk-images>
-	<template each={ image in images }>
-		<mk-images-image image={ image }/>
-	</template>
-	<style lang="stylus" scoped>
-		:scope
-			display grid
-			grid-gap 4px
-			height 256px
-	</style>
-	<script lang="typescript">
-		this.images = this.opts.images;
-
-		this.on('mount', () => {
-			if (this.images.length == 1) {
-				this.root.style.gridTemplateRows = '1fr';
-
-				this.tags['mk-images-image'].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-image'].root.style.gridRow = '1 / 2';
-			} else if (this.images.length == 2) {
-				this.root.style.gridTemplateColumns = '1fr 1fr';
-				this.root.style.gridTemplateRows = '1fr';
-
-				this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
-			} else if (this.images.length == 3) {
-				this.root.style.gridTemplateColumns = '1fr 0.5fr';
-				this.root.style.gridTemplateRows = '1fr 1fr';
-
-				this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-image'][0].root.style.gridRow = '1 / 3';
-				this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-image'][2].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3';
-			} else if (this.images.length == 4) {
-				this.root.style.gridTemplateColumns = '1fr 1fr';
-				this.root.style.gridTemplateRows = '1fr 1fr';
-
-				this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-image'][2].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3';
-				this.tags['mk-images-image'][3].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-image'][3].root.style.gridRow = '2 / 3';
-			}
-		});
-	</script>
-</mk-images>
-
-<mk-images-image>
-	<a ref="view"
-		href={ image.url }
-		onmousemove={ mousemove }
-		onmouseleave={ mouseleave }
-		style={ styles }
-		@click="click"
-		title={ image.name }></a>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow hidden
-			border-radius 4px
-
-			> a
-				display block
-				cursor zoom-in
-				overflow hidden
-				width 100%
-				height 100%
-				background-position center
-
-				&:not(:hover)
-					background-size cover
-
-	</style>
-	<script lang="typescript">
-		this.image = this.opts.image;
-		this.styles = {
-			'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
-			'background-image': `url(${this.image.url}?thumbnail&size=512)`
-		};
-
-		this.mousemove = e => {
-			const rect = this.$refs.view.getBoundingClientRect();
-			const mouseX = e.clientX - rect.left;
-			const mouseY = e.clientY - rect.top;
-			const xp = mouseX / this.$refs.view.offsetWidth * 100;
-			const yp = mouseY / this.$refs.view.offsetHeight * 100;
-			this.$refs.view.style.backgroundPosition = xp + '% ' + yp + '%';
-			this.$refs.view.style.backgroundImage = 'url("' + this.image.url + '?thumbnail")';
-		};
-
-		this.mouseleave = () => {
-			this.$refs.view.style.backgroundPosition = '';
-		};
-
-		this.click = ev => {
-			ev.preventDefault();
-			riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), {
-				image: this.image
-			});
-			return false;
-		};
-	</script>
-</mk-images-image>
-
-<mk-image-dialog>
-	<div class="bg" ref="bg" @click="close"></div><img ref="img" src={ image.url } alt={ image.name } title={ image.name } @click="close"/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			position fixed
-			z-index 2048
-			top 0
-			left 0
-			width 100%
-			height 100%
-			opacity 0
-
-			> .bg
-				display block
-				position fixed
-				z-index 1
-				top 0
-				left 0
-				width 100%
-				height 100%
-				background rgba(0, 0, 0, 0.7)
-
-			> img
-				position fixed
-				z-index 2
-				top 0
-				right 0
-				bottom 0
-				left 0
-				max-width 100%
-				max-height 100%
-				margin auto
-				cursor zoom-out
-
-	</style>
-	<script lang="typescript">
-		import anime from 'animejs';
-
-		this.image = this.opts.image;
-
-		this.on('mount', () => {
-			anime({
-				targets: this.root,
-				opacity: 1,
-				duration: 100,
-				easing: 'linear'
-			});
-		});
-
-		this.close = () => {
-			anime({
-				targets: this.root,
-				opacity: 0,
-				duration: 100,
-				easing: 'linear',
-				complete: () => this.$destroy()
-			});
-		};
-	</script>
-</mk-image-dialog>
diff --git a/src/web/app/desktop/views/components/images-image-dialog.vue b/src/web/app/desktop/views/components/images-image-dialog.vue
new file mode 100644
index 000000000..7975d8061
--- /dev/null
+++ b/src/web/app/desktop/views/components/images-image-dialog.vue
@@ -0,0 +1,69 @@
+<template>
+<div class="mk-images-image-dialog">
+	<div class="bg" @click="close"></div>
+	<img :src="image.url" :alt="image.name" :title="image.name" @click="close"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import anime from 'animejs';
+
+export default Vue.extend({
+	props: ['image'],
+	mounted() {
+		anime({
+			targets: this.$el,
+			opacity: 1,
+			duration: 100,
+			easing: 'linear'
+		});
+	},
+	methods: {
+		close() {
+			anime({
+				targets: this.$el,
+				opacity: 0,
+				duration: 100,
+				easing: 'linear',
+				complete: () => this.$destroy()
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-images-image-dialog
+	display block
+	position fixed
+	z-index 2048
+	top 0
+	left 0
+	width 100%
+	height 100%
+	opacity 0
+
+	> .bg
+		display block
+		position fixed
+		z-index 1
+		top 0
+		left 0
+		width 100%
+		height 100%
+		background rgba(0, 0, 0, 0.7)
+
+	> img
+		position fixed
+		z-index 2
+		top 0
+		right 0
+		bottom 0
+		left 0
+		max-width 100%
+		max-height 100%
+		margin auto
+		cursor zoom-out
+
+</style>
diff --git a/src/web/app/desktop/views/components/images-image.vue b/src/web/app/desktop/views/components/images-image.vue
new file mode 100644
index 000000000..ac662449f
--- /dev/null
+++ b/src/web/app/desktop/views/components/images-image.vue
@@ -0,0 +1,66 @@
+<template>
+<a class="mk-images-image"
+	:href="image.url"
+	@mousemove="onMousemove"
+	@mouseleave="onMouseleave"
+	@click.prevent="onClick"
+	:style="styles"
+	:title="image.name"></a>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['image'],
+	computed: {
+		style(): any {
+			return {
+				'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
+				'background-image': `url(${this.image.url}?thumbnail&size=512)`
+			};
+		}
+	},
+	methods: {
+		onMousemove(e) {
+			const rect = this.$refs.view.getBoundingClientRect();
+			const mouseX = e.clientX - rect.left;
+			const mouseY = e.clientY - rect.top;
+			const xp = mouseX / this.$el.offsetWidth * 100;
+			const yp = mouseY / this.$el.offsetHeight * 100;
+			this.$el.style.backgroundPosition = xp + '% ' + yp + '%';
+			this.$el.style.backgroundImage = 'url("' + this.image.url + '?thumbnail")';
+		},
+
+		onMouseleave() {
+			this.$el.style.backgroundPosition = '';
+		},
+
+		onClick(ev) {
+			riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), {
+				image: this.image
+			});
+			return false;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-images-image
+	display block
+	overflow hidden
+	border-radius 4px
+
+	> a
+		display block
+		cursor zoom-in
+		overflow hidden
+		width 100%
+		height 100%
+		background-position center
+
+		&:not(:hover)
+			background-size cover
+
+</style>
diff --git a/src/web/app/desktop/views/components/images.vue b/src/web/app/desktop/views/components/images.vue
new file mode 100644
index 000000000..fb2532753
--- /dev/null
+++ b/src/web/app/desktop/views/components/images.vue
@@ -0,0 +1,60 @@
+<template>
+<div class="mk-images">
+	<mk-images-image v-for="image in images" ref="image" :image="image" :key="image.id"/>
+</div>
+</template>
+
+<style lang="stylus" scoped>
+.mk-images
+	display grid
+	grid-gap 4px
+	height 256px
+</style>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['images'],
+	mounted() {
+		const tags = this.$refs.image as Vue[];
+
+		if (this.images.length == 1) {
+			this.$el.style.gridTemplateRows = '1fr';
+
+			tags[0].$el.style.gridColumn = '1 / 2';
+			tags[0].$el.style.gridRow = '1 / 2';
+		} else if (this.images.length == 2) {
+			this.$el.style.gridTemplateColumns = '1fr 1fr';
+			this.$el.style.gridTemplateRows = '1fr';
+
+			tags[0].$el.style.gridColumn = '1 / 2';
+			tags[0].$el.style.gridRow = '1 / 2';
+			tags[1].$el.style.gridColumn = '2 / 3';
+			tags[1].$el.style.gridRow = '1 / 2';
+		} else if (this.images.length == 3) {
+			this.$el.style.gridTemplateColumns = '1fr 0.5fr';
+			this.$el.style.gridTemplateRows = '1fr 1fr';
+
+			tags[0].$el.style.gridColumn = '1 / 2';
+			tags[0].$el.style.gridRow = '1 / 3';
+			tags[1].$el.style.gridColumn = '2 / 3';
+			tags[1].$el.style.gridRow = '1 / 2';
+			tags[2].$el.style.gridColumn = '2 / 3';
+			tags[2].$el.style.gridRow = '2 / 3';
+		} else if (this.images.length == 4) {
+			this.$el.style.gridTemplateColumns = '1fr 1fr';
+			this.$el.style.gridTemplateRows = '1fr 1fr';
+
+			tags[0].$el.style.gridColumn = '1 / 2';
+			tags[0].$el.style.gridRow = '1 / 2';
+			tags[1].$el.style.gridColumn = '2 / 3';
+			tags[1].$el.style.gridRow = '1 / 2';
+			tags[2].$el.style.gridColumn = '1 / 2';
+			tags[2].$el.style.gridRow = '2 / 3';
+			tags[3].$el.style.gridColumn = '2 / 3';
+			tags[3].$el.style.gridRow = '2 / 3';
+		}
+	}
+});
+</script>

From ae1338f76160d8cd9e640c4854a506ab8b0e0b57 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 12:21:26 +0900
Subject: [PATCH 053/286] wip

---
 .../desktop/views/components/post-form.vue    | 35 ++++++++++++++-----
 1 file changed, 27 insertions(+), 8 deletions(-)

diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index d021c9ab5..52efaf849 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -6,19 +6,19 @@
 	@drop="onDrop"
 >
 	<div class="content">
-		<textarea :class="{ with: (files.length != 0 || poll) }" ref="text" :disabled="wait"
+		<textarea :class="{ with: (files.length != 0 || poll) }" ref="text" :disabled="posting"
 			@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
 		></textarea>
 		<div class="medias" :class="{ with: poll }" v-show="files.length != 0">
 			<ul ref="media">
-				<li each={ files } data-id={ id }>
-					<div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name }></div>
-					<img class="remove" @click="removeFile" src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/>
+				<li v-for="file in files" :key="file.id">
+					<div class="img" :style="{ backgroundImage: `url(${file.url}?thumbnail&size=64)` }" :title="file.name"></div>
+					<img class="remove" @click="removeFile(file.id)" src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/>
 				</li>
 			</ul>
-			<p class="remain">{ 4 - files.length }/4</p>
+			<p class="remain">{{ 4 - files.length }}/4</p>
 		</div>
-		<mk-poll-editor v-if="poll" ref="poll" ondestroy={ onPollDestroyed }/>
+		<mk-poll-editor v-if="poll" ref="poll" @destroyed="onPollDestroyed"/>
 	</div>
 	<mk-uploader ref="uploader"/>
 	<button ref="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" @click="selectFile">%fa:upload%</button>
@@ -26,10 +26,29 @@
 	<button class="kao" title="%i18n:desktop.tags.mk-post-form.insert-a-kao%" @click="kao">%fa:R smile%</button>
 	<button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" @click="addPoll">%fa:chart-pie%</button>
 	<p class="text-count { over: refs.text.value.length > 1000 }">{ '%i18n:desktop.tags.mk-post-form.text-remain%'.replace('{}', 1000 - refs.text.value.length) }</p>
-	<button :class="{ wait: wait }" ref="submit" disabled={ wait || (refs.text.value.length == 0 && files.length == 0 && !poll && !repost) } @click="post">
-		{ wait ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }<mk-ellipsis v-if="wait"/>
+	<button :class="{ posting }" ref="submit" :disabled="!canPost" @click="post">
+		{ posting ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }<mk-ellipsis v-if="posting"/>
 	</button>
 	<input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" onchange={ changeFile }/>
 	<div class="dropzone" v-if="draghover"></div>
 </div>
 </template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	data() {
+		return {
+			posting: false,
+
+		};
+	},
+	computed: {
+		canPost(): boolean {
+			return !this.posting && (refs.text.value.length != 0 || files.length != 0 || poll || repost);
+		}
+	}
+});
+</script>
+

From 99f6e1a2e1f6066b188c3c09486bfa1c0a5302f1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 18:49:06 +0900
Subject: [PATCH 054/286] wip

---
 src/web/app/desktop/-tags/post-form.tag       | 540 ------------------
 .../desktop/views/components/post-form.vue    | 476 ++++++++++++++-
 2 files changed, 465 insertions(+), 551 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/post-form.tag

diff --git a/src/web/app/desktop/-tags/post-form.tag b/src/web/app/desktop/-tags/post-form.tag
deleted file mode 100644
index ddbb485d9..000000000
--- a/src/web/app/desktop/-tags/post-form.tag
+++ /dev/null
@@ -1,540 +0,0 @@
-<mk-post-form ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop }>
-	<div class="content">
-		<textarea :class="{ with: (files.length != 0 || poll) }" ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ placeholder }></textarea>
-		<div class="medias { with: poll }" show={ files.length != 0 }>
-			<ul ref="media">
-				<li each={ files } data-id={ id }>
-					<div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name }></div>
-					<img class="remove" @click="removeFile" src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/>
-				</li>
-			</ul>
-			<p class="remain">{ 4 - files.length }/4</p>
-		</div>
-		<mk-poll-editor v-if="poll" ref="poll" ondestroy={ onPollDestroyed }/>
-	</div>
-	<mk-uploader ref="uploader"/>
-	<button ref="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" @click="selectFile">%fa:upload%</button>
-	<button ref="drive" title="%i18n:desktop.tags.mk-post-form.attach-media-from-drive%" @click="selectFileFromDrive">%fa:cloud%</button>
-	<button class="kao" title="%i18n:desktop.tags.mk-post-form.insert-a-kao%" @click="kao">%fa:R smile%</button>
-	<button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" @click="addPoll">%fa:chart-pie%</button>
-	<p class="text-count { over: refs.text.value.length > 1000 }">{ '%i18n:desktop.tags.mk-post-form.text-remain%'.replace('{}', 1000 - refs.text.value.length) }</p>
-	<button :class="{ wait: wait }" ref="submit" disabled={ wait || (refs.text.value.length == 0 && files.length == 0 && !poll && !repost) } @click="post">
-		{ wait ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }<mk-ellipsis v-if="wait"/>
-	</button>
-	<input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" onchange={ changeFile }/>
-	<div class="dropzone" v-if="draghover"></div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding 16px
-			background lighten($theme-color, 95%)
-
-			&:after
-				content ""
-				display block
-				clear both
-
-			> .content
-
-				[ref='text']
-					display block
-					padding 12px
-					margin 0
-					width 100%
-					max-width 100%
-					min-width 100%
-					min-height calc(16px + 12px + 12px)
-					font-size 16px
-					color #333
-					background #fff
-					outline none
-					border solid 1px rgba($theme-color, 0.1)
-					border-radius 4px
-					transition border-color .3s ease
-
-					&:hover
-						border-color rgba($theme-color, 0.2)
-						transition border-color .1s ease
-
-						& + *
-						& + * + *
-							border-color rgba($theme-color, 0.2)
-							transition border-color .1s ease
-
-					&:focus
-						color $theme-color
-						border-color rgba($theme-color, 0.5)
-						transition border-color 0s ease
-
-						& + *
-						& + * + *
-							border-color rgba($theme-color, 0.5)
-							transition border-color 0s ease
-
-					&:disabled
-						opacity 0.5
-
-					&::-webkit-input-placeholder
-						color rgba($theme-color, 0.3)
-
-					&.with
-						border-bottom solid 1px rgba($theme-color, 0.1) !important
-						border-radius 4px 4px 0 0
-
-				> .medias
-					margin 0
-					padding 0
-					background lighten($theme-color, 98%)
-					border solid 1px rgba($theme-color, 0.1)
-					border-top none
-					border-radius 0 0 4px 4px
-					transition border-color .3s ease
-
-					&.with
-						border-bottom solid 1px rgba($theme-color, 0.1) !important
-						border-radius 0
-
-					> .remain
-						display block
-						position absolute
-						top 8px
-						right 8px
-						margin 0
-						padding 0
-						color rgba($theme-color, 0.4)
-
-					> ul
-						display block
-						margin 0
-						padding 4px
-						list-style none
-
-						&:after
-							content ""
-							display block
-							clear both
-
-						> li
-							display block
-							float left
-							margin 0
-							padding 0
-							border solid 4px transparent
-							cursor move
-
-							&:hover > .remove
-								display block
-
-							> .img
-								width 64px
-								height 64px
-								background-size cover
-								background-position center center
-
-							> .remove
-								display none
-								position absolute
-								top -6px
-								right -6px
-								width 16px
-								height 16px
-								cursor pointer
-
-				> mk-poll-editor
-					background lighten($theme-color, 98%)
-					border solid 1px rgba($theme-color, 0.1)
-					border-top none
-					border-radius 0 0 4px 4px
-					transition border-color .3s ease
-
-			> mk-uploader
-				margin 8px 0 0 0
-				padding 8px
-				border solid 1px rgba($theme-color, 0.2)
-				border-radius 4px
-
-			[ref='file']
-				display none
-
-			.text-count
-				pointer-events none
-				display block
-				position absolute
-				bottom 16px
-				right 138px
-				margin 0
-				line-height 40px
-				color rgba($theme-color, 0.5)
-
-				&.over
-					color #ec3828
-
-			[ref='submit']
-				display block
-				position absolute
-				bottom 16px
-				right 16px
-				cursor pointer
-				padding 0
-				margin 0
-				width 110px
-				height 40px
-				font-size 1em
-				color $theme-color-foreground
-				background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
-				outline none
-				border solid 1px lighten($theme-color, 15%)
-				border-radius 4px
-
-				&:not(:disabled)
-					font-weight bold
-
-				&:hover:not(:disabled)
-					background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
-					border-color $theme-color
-
-				&:active:not(:disabled)
-					background $theme-color
-					border-color $theme-color
-
-				&:focus
-					&:after
-						content ""
-						pointer-events none
-						position absolute
-						top -5px
-						right -5px
-						bottom -5px
-						left -5px
-						border 2px solid rgba($theme-color, 0.3)
-						border-radius 8px
-
-				&:disabled
-					opacity 0.7
-					cursor default
-
-				&.wait
-					background linear-gradient(
-						45deg,
-						darken($theme-color, 10%) 25%,
-						$theme-color              25%,
-						$theme-color              50%,
-						darken($theme-color, 10%) 50%,
-						darken($theme-color, 10%) 75%,
-						$theme-color              75%,
-						$theme-color
-					)
-					background-size 32px 32px
-					animation stripe-bg 1.5s linear infinite
-					opacity 0.7
-					cursor wait
-
-					@keyframes stripe-bg
-						from {background-position: 0 0;}
-						to   {background-position: -64px 32px;}
-
-			[ref='upload']
-			[ref='drive']
-			.kao
-			.poll
-				display inline-block
-				cursor pointer
-				padding 0
-				margin 8px 4px 0 0
-				width 40px
-				height 40px
-				font-size 1em
-				color rgba($theme-color, 0.5)
-				background transparent
-				outline none
-				border solid 1px transparent
-				border-radius 4px
-
-				&:hover
-					background transparent
-					border-color rgba($theme-color, 0.3)
-
-				&:active
-					color rgba($theme-color, 0.6)
-					background 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
-
-				&:focus
-					&:after
-						content ""
-						pointer-events none
-						position absolute
-						top -5px
-						right -5px
-						bottom -5px
-						left -5px
-						border 2px solid rgba($theme-color, 0.3)
-						border-radius 8px
-
-			> .dropzone
-				position absolute
-				left 0
-				top 0
-				width 100%
-				height 100%
-				border dashed 2px rgba($theme-color, 0.5)
-				pointer-events none
-
-	</style>
-	<script lang="typescript">
-		import Sortable from 'sortablejs';
-		import getKao from '../../common/scripts/get-kao';
-		import notify from '../scripts/notify';
-		import Autocomplete from '../scripts/autocomplete';
-
-		this.mixin('api');
-
-		this.wait = false;
-		this.uploadings = [];
-		this.files = [];
-		this.autocomplete = null;
-		this.poll = false;
-
-		this.inReplyToPost = this.opts.reply;
-
-		this.repost = this.opts.repost;
-
-		this.placeholder = this.repost
-			? '%i18n:desktop.tags.mk-post-form.quote-placeholder%'
-			: this.inReplyToPost
-				? '%i18n:desktop.tags.mk-post-form.reply-placeholder%'
-				: '%i18n:desktop.tags.mk-post-form.post-placeholder%';
-
-		this.submitText = this.repost
-			? '%i18n:desktop.tags.mk-post-form.repost%'
-			: this.inReplyToPost
-				? '%i18n:desktop.tags.mk-post-form.reply%'
-				: '%i18n:desktop.tags.mk-post-form.post%';
-
-		this.draftId = this.repost
-			? 'repost:' + this.repost.id
-			: this.inReplyToPost
-				? 'reply:' + this.inReplyToPost.id
-				: 'post';
-
-		this.on('mount', () => {
-			this.$refs.uploader.on('uploaded', file => {
-				this.addFile(file);
-			});
-
-			this.$refs.uploader.on('change-uploads', uploads => {
-				this.$emit('change-uploading-files', uploads);
-			});
-
-			this.autocomplete = new Autocomplete(this.$refs.text);
-			this.autocomplete.attach();
-
-			// 書きかけの投稿を復元
-			const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
-			if (draft) {
-				this.$refs.text.value = draft.data.text;
-				this.files = draft.data.files;
-				if (draft.data.poll) {
-					this.poll = true;
-					this.update();
-					this.$refs.poll.set(draft.data.poll);
-				}
-				this.$emit('change-files', this.files);
-				this.update();
-			}
-
-			new Sortable(this.$refs.media, {
-				animation: 150
-			});
-		});
-
-		this.on('unmount', () => {
-			this.autocomplete.detach();
-		});
-
-		this.focus = () => {
-			this.$refs.text.focus();
-		};
-
-		this.clear = () => {
-			this.$refs.text.value = '';
-			this.files = [];
-			this.poll = false;
-			this.$emit('change-files');
-			this.update();
-		};
-
-		this.ondragover = e => {
-			e.preventDefault();
-			e.stopPropagation();
-			this.draghover = true;
-			e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-		};
-
-		this.ondragenter = e => {
-			this.draghover = true;
-		};
-
-		this.ondragleave = e => {
-			this.draghover = false;
-		};
-
-		this.ondrop = e => {
-			e.preventDefault();
-			e.stopPropagation();
-			this.draghover = false;
-
-			// ファイルだったら
-			if (e.dataTransfer.files.length > 0) {
-				Array.from(e.dataTransfer.files).forEach(this.upload);
-				return;
-			}
-
-			// データ取得
-			const data = e.dataTransfer.getData('text');
-			if (data == null) return false;
-
-			try {
-				// パース
-				const obj = JSON.parse(data);
-
-				// (ドライブの)ファイルだったら
-				if (obj.type == 'file') {
-					this.files.push(obj.file);
-					this.update();
-				}
-			} catch (e) {
-
-			}
-		};
-
-		this.onkeydown = e => {
-			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
-		};
-
-		this.onpaste = e => {
-			Array.from(e.clipboardData.items).forEach(item => {
-				if (item.kind == 'file') {
-					this.upload(item.getAsFile());
-				}
-			});
-		};
-
-		this.selectFile = () => {
-			this.$refs.file.click();
-		};
-
-		this.selectFileFromDrive = () => {
-			const i = riot.mount(document.body.appendChild(document.createElement('mk-select-file-from-drive-window')), {
-				multiple: true
-			})[0];
-			i.one('selected', files => {
-				files.forEach(this.addFile);
-			});
-		};
-
-		this.changeFile = () => {
-			Array.from(this.$refs.file.files).forEach(this.upload);
-		};
-
-		this.upload = file => {
-			this.$refs.uploader.upload(file);
-		};
-
-		this.addFile = file => {
-			this.files.push(file);
-			this.$emit('change-files', this.files);
-			this.update();
-		};
-
-		this.removeFile = e => {
-			const file = e.item;
-			this.files = this.files.filter(x => x.id != file.id);
-			this.$emit('change-files', this.files);
-			this.update();
-		};
-
-		this.addPoll = () => {
-			this.poll = true;
-		};
-
-		this.onPollDestroyed = () => {
-			this.update({
-				poll: false
-			});
-		};
-
-		this.post = e => {
-			this.wait = true;
-
-			const files = [];
-
-			if (this.files.length > 0) {
-				Array.from(this.$refs.media.children).forEach(el => {
-					const id = el.getAttribute('data-id');
-					const file = this.files.find(f => f.id == id);
-					files.push(file);
-				});
-			}
-
-			this.api('posts/create', {
-				text: this.$refs.text.value == '' ? undefined : this.$refs.text.value,
-				media_ids: this.files.length > 0 ? files.map(f => f.id) : undefined,
-				reply_id: this.inReplyToPost ? this.inReplyToPost.id : undefined,
-				repost_id: this.repost ? this.repost.id : undefined,
-				poll: this.poll ? this.$refs.poll.get() : undefined
-			}).then(data => {
-				this.clear();
-				this.removeDraft();
-				this.$emit('post');
-				notify(this.repost
-					? '%i18n:desktop.tags.mk-post-form.reposted%'
-					: this.inReplyToPost
-						? '%i18n:desktop.tags.mk-post-form.replied%'
-						: '%i18n:desktop.tags.mk-post-form.posted%');
-			}).catch(err => {
-				notify(this.repost
-					? '%i18n:desktop.tags.mk-post-form.repost-failed%'
-					: this.inReplyToPost
-						? '%i18n:desktop.tags.mk-post-form.reply-failed%'
-						: '%i18n:desktop.tags.mk-post-form.post-failed%');
-			}).then(() => {
-				this.update({
-					wait: false
-				});
-			});
-		};
-
-		this.kao = () => {
-			this.$refs.text.value += getKao();
-		};
-
-		this.on('update', () => {
-			this.saveDraft();
-		});
-
-		this.saveDraft = () => {
-			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
-
-			data[this.draftId] = {
-				updated_at: new Date(),
-				data: {
-					text: this.$refs.text.value,
-					files: this.files,
-					poll: this.poll && this.$refs.poll ? this.$refs.poll.get() : undefined
-				}
-			}
-
-			localStorage.setItem('drafts', JSON.stringify(data));
-		};
-
-		this.removeDraft = () => {
-			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
-
-			delete data[this.draftId];
-
-			localStorage.setItem('drafts', JSON.stringify(data));
-		};
-	</script>
-</mk-post-form>
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 52efaf849..9efca5ddc 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -1,54 +1,508 @@
 <template>
 <div class="mk-post-form"
-	@dragover="onDragover"
+	@dragover.prevent.stop="onDragover"
 	@dragenter="onDragenter"
 	@dragleave="onDragleave"
-	@drop="onDrop"
+	@drop.prevent.stop="onDrop"
 >
 	<div class="content">
-		<textarea :class="{ with: (files.length != 0 || poll) }" ref="text" :disabled="posting"
+		<textarea :class="{ with: (files.length != 0 || poll) }"
+			ref="text" v-model="text" :disabled="posting"
 			@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
 		></textarea>
 		<div class="medias" :class="{ with: poll }" v-show="files.length != 0">
 			<ul ref="media">
 				<li v-for="file in files" :key="file.id">
 					<div class="img" :style="{ backgroundImage: `url(${file.url}?thumbnail&size=64)` }" :title="file.name"></div>
-					<img class="remove" @click="removeFile(file.id)" src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/>
+					<img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/>
 				</li>
 			</ul>
 			<p class="remain">{{ 4 - files.length }}/4</p>
 		</div>
-		<mk-poll-editor v-if="poll" ref="poll" @destroyed="onPollDestroyed"/>
+		<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false"/>
 	</div>
-	<mk-uploader ref="uploader"/>
+	<mk-uploader @uploaded="attachMedia" @change="onChangeUploadings"/>
 	<button ref="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" @click="selectFile">%fa:upload%</button>
 	<button ref="drive" title="%i18n:desktop.tags.mk-post-form.attach-media-from-drive%" @click="selectFileFromDrive">%fa:cloud%</button>
 	<button class="kao" title="%i18n:desktop.tags.mk-post-form.insert-a-kao%" @click="kao">%fa:R smile%</button>
-	<button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" @click="addPoll">%fa:chart-pie%</button>
+	<button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" @click="poll = true">%fa:chart-pie%</button>
 	<p class="text-count { over: refs.text.value.length > 1000 }">{ '%i18n:desktop.tags.mk-post-form.text-remain%'.replace('{}', 1000 - refs.text.value.length) }</p>
 	<button :class="{ posting }" ref="submit" :disabled="!canPost" @click="post">
-		{ posting ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }<mk-ellipsis v-if="posting"/>
+		{{ posting ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }}<mk-ellipsis v-if="posting"/>
 	</button>
-	<input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" onchange={ changeFile }/>
+	<input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
 	<div class="dropzone" v-if="draghover"></div>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import Sortable from 'sortablejs';
+import Autocomplete from '../../scripts/autocomplete';
+import getKao from '../../../common/scripts/get-kao';
+import notify from '../../scripts/notify';
 
 export default Vue.extend({
+	props: ['reply', 'repost'],
 	data() {
 		return {
 			posting: false,
-
+			text: '',
+			files: [],
+			uploadings: [],
+			poll: false,
+			autocomplete: null,
+			draghover: false
 		};
 	},
 	computed: {
+		draftId(): string {
+			return this.repost
+				? 'repost:' + this.repost.id
+				: this.reply
+					? 'reply:' + this.reply.id
+					: 'post';
+		},
+		placeholder(): string {
+			return this.repost
+				? '%i18n:desktop.tags.mk-post-form.quote-placeholder%'
+				: this.reply
+					? '%i18n:desktop.tags.mk-post-form.reply-placeholder%'
+					: '%i18n:desktop.tags.mk-post-form.post-placeholder%';
+		},
+		submitText(): string {
+			return this.repost
+				? '%i18n:desktop.tags.mk-post-form.repost%'
+				: this.reply
+					? '%i18n:desktop.tags.mk-post-form.reply%'
+					: '%i18n:desktop.tags.mk-post-form.post%';
+		},
 		canPost(): boolean {
-			return !this.posting && (refs.text.value.length != 0 || files.length != 0 || poll || repost);
+			return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.repost);
+		}
+	},
+	mounted() {
+		this.autocomplete = new Autocomplete(this.$refs.text);
+		this.autocomplete.attach();
+
+		// 書きかけの投稿を復元
+		const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
+		if (draft) {
+			this.text = draft.data.text;
+			this.files = draft.data.files;
+			if (draft.data.poll) {
+				this.poll = true;
+				(this.$refs.poll as any).set(draft.data.poll);
+			}
+			this.$emit('change-attached-media', this.files);
+		}
+
+		new Sortable(this.$refs.media, {
+			animation: 150
+		});
+	},
+	beforeDestroy() {
+		this.autocomplete.detach();
+	},
+	methods: {
+		focus() {
+			(this.$refs.text as any).focus();
+		},
+		chooseFile() {
+			(this.$refs.file as any).click();
+		},
+		chooseFileFromDrive() {
+			const w = new MkDriveFileSelectorWindow({
+				propsData: {
+					multiple: true
+				}
+			}).$mount();
+
+			document.body.appendChild(w.$el);
+
+			w.$once('selected', files => {
+				files.forEach(this.attachMedia);
+			});
+		},
+		attachMedia(driveFile) {
+			this.files.push(driveFile);
+			this.$emit('change-attached-media', this.files);
+		},
+		detachMedia(id) {
+			this.files = this.files.filter(x => x.id != id);
+			this.$emit('change-attached-media', this.files);
+		},
+		onChangeFile() {
+			Array.from((this.$refs.file as any).files).forEach(this.upload);
+		},
+		upload(file) {
+			(this.$refs.uploader as any).upload(file);
+		},
+		onChangeUploadings(uploads) {
+			this.$emit('change-uploadings', uploads);
+		},
+		clear() {
+			this.text = '';
+			this.files = [];
+			this.poll = false;
+			this.$emit('change-attached-media');
+		},
+		onKeydown(e) {
+			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
+		},
+		onPaste(e) {
+			Array.from(e.clipboardData.items).forEach((item: any) => {
+				if (item.kind == 'file') {
+					this.upload(item.getAsFile());
+				}
+			});
+		},
+		onDragover(e) {
+			this.draghover = true;
+			e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+		},
+		onDragenter(e) {
+			this.draghover = true;
+		},
+		onDragleave(e) {
+			this.draghover = false;
+		},
+		onDrop(e): void {
+			this.draghover = false;
+
+			// ファイルだったら
+			if (e.dataTransfer.files.length > 0) {
+				Array.from(e.dataTransfer.files).forEach(this.upload);
+				return;
+			}
+
+			// データ取得
+			const data = e.dataTransfer.getData('text');
+			if (data == null) return;
+
+			try {
+				// パース
+				const obj = JSON.parse(data);
+
+				// (ドライブの)ファイルだったら
+				if (obj.type == 'file') {
+					this.files.push(obj.file);
+					this.$emit('change-attached-media');
+				}
+			} catch (e) { }
+		},
+		post() {
+			this.posting = true;
+
+			this.$root.$data.os.api('posts/create', {
+				text: this.text == '' ? undefined : this.text,
+				media_ids: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
+				reply_id: this.reply ? this.reply.id : undefined,
+				repost_id: this.repost ? this.repost.id : undefined,
+				poll: this.poll ? (this.$refs.poll as any).get() : undefined
+			}).then(data => {
+				this.clear();
+				this.deleteDraft();
+				this.$emit('posted');
+				notify(this.repost
+					? '%i18n:desktop.tags.mk-post-form.reposted%'
+					: this.reply
+						? '%i18n:desktop.tags.mk-post-form.replied%'
+						: '%i18n:desktop.tags.mk-post-form.posted%');
+			}).catch(err => {
+				notify(this.repost
+					? '%i18n:desktop.tags.mk-post-form.repost-failed%'
+					: this.reply
+						? '%i18n:desktop.tags.mk-post-form.reply-failed%'
+						: '%i18n:desktop.tags.mk-post-form.post-failed%');
+			}).then(() => {
+				this.posting = false;
+			});
+		},
+		saveDraft() {
+			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
+
+			data[this.draftId] = {
+				updated_at: new Date(),
+				data: {
+					text: this.text,
+					files: this.files,
+					poll: this.poll && this.$refs.poll ? (this.$refs.poll as any).get() : undefined
+				}
+			}
+
+			localStorage.setItem('drafts', JSON.stringify(data));
+		},
+		deleteDraft() {
+			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
+
+			delete data[this.draftId];
+
+			localStorage.setItem('drafts', JSON.stringify(data));
+		},
+		kao() {
+			this.text += getKao();
 		}
 	}
 });
 </script>
 
+<style lang="stylus" scoped>
+.mk-post-form
+	display block
+	padding 16px
+	background lighten($theme-color, 95%)
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	> .content
+
+		[ref='text']
+			display block
+			padding 12px
+			margin 0
+			width 100%
+			max-width 100%
+			min-width 100%
+			min-height calc(16px + 12px + 12px)
+			font-size 16px
+			color #333
+			background #fff
+			outline none
+			border solid 1px rgba($theme-color, 0.1)
+			border-radius 4px
+			transition border-color .3s ease
+
+			&:hover
+				border-color rgba($theme-color, 0.2)
+				transition border-color .1s ease
+
+				& + *
+				& + * + *
+					border-color rgba($theme-color, 0.2)
+					transition border-color .1s ease
+
+			&:focus
+				color $theme-color
+				border-color rgba($theme-color, 0.5)
+				transition border-color 0s ease
+
+				& + *
+				& + * + *
+					border-color rgba($theme-color, 0.5)
+					transition border-color 0s ease
+
+			&:disabled
+				opacity 0.5
+
+			&::-webkit-input-placeholder
+				color rgba($theme-color, 0.3)
+
+			&.with
+				border-bottom solid 1px rgba($theme-color, 0.1) !important
+				border-radius 4px 4px 0 0
+
+		> .medias
+			margin 0
+			padding 0
+			background lighten($theme-color, 98%)
+			border solid 1px rgba($theme-color, 0.1)
+			border-top none
+			border-radius 0 0 4px 4px
+			transition border-color .3s ease
+
+			&.with
+				border-bottom solid 1px rgba($theme-color, 0.1) !important
+				border-radius 0
+
+			> .remain
+				display block
+				position absolute
+				top 8px
+				right 8px
+				margin 0
+				padding 0
+				color rgba($theme-color, 0.4)
+
+			> ul
+				display block
+				margin 0
+				padding 4px
+				list-style none
+
+				&:after
+					content ""
+					display block
+					clear both
+
+				> li
+					display block
+					float left
+					margin 0
+					padding 0
+					border solid 4px transparent
+					cursor move
+
+					&:hover > .remove
+						display block
+
+					> .img
+						width 64px
+						height 64px
+						background-size cover
+						background-position center center
+
+					> .remove
+						display none
+						position absolute
+						top -6px
+						right -6px
+						width 16px
+						height 16px
+						cursor pointer
+
+		> mk-poll-editor
+			background lighten($theme-color, 98%)
+			border solid 1px rgba($theme-color, 0.1)
+			border-top none
+			border-radius 0 0 4px 4px
+			transition border-color .3s ease
+
+	> mk-uploader
+		margin 8px 0 0 0
+		padding 8px
+		border solid 1px rgba($theme-color, 0.2)
+		border-radius 4px
+
+	[ref='file']
+		display none
+
+	.text-count
+		pointer-events none
+		display block
+		position absolute
+		bottom 16px
+		right 138px
+		margin 0
+		line-height 40px
+		color rgba($theme-color, 0.5)
+
+		&.over
+			color #ec3828
+
+	[ref='submit']
+		display block
+		position absolute
+		bottom 16px
+		right 16px
+		cursor pointer
+		padding 0
+		margin 0
+		width 110px
+		height 40px
+		font-size 1em
+		color $theme-color-foreground
+		background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+		outline none
+		border solid 1px lighten($theme-color, 15%)
+		border-radius 4px
+
+		&:not(:disabled)
+			font-weight bold
+
+		&:hover:not(:disabled)
+			background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
+			border-color $theme-color
+
+		&:active:not(:disabled)
+			background $theme-color
+			border-color $theme-color
+
+		&:focus
+			&:after
+				content ""
+				pointer-events none
+				position absolute
+				top -5px
+				right -5px
+				bottom -5px
+				left -5px
+				border 2px solid rgba($theme-color, 0.3)
+				border-radius 8px
+
+		&:disabled
+			opacity 0.7
+			cursor default
+
+		&.wait
+			background linear-gradient(
+				45deg,
+				darken($theme-color, 10%) 25%,
+				$theme-color              25%,
+				$theme-color              50%,
+				darken($theme-color, 10%) 50%,
+				darken($theme-color, 10%) 75%,
+				$theme-color              75%,
+				$theme-color
+			)
+			background-size 32px 32px
+			animation stripe-bg 1.5s linear infinite
+			opacity 0.7
+			cursor wait
+
+			@keyframes stripe-bg
+				from {background-position: 0 0;}
+				to   {background-position: -64px 32px;}
+
+	[ref='upload']
+	[ref='drive']
+	.kao
+	.poll
+		display inline-block
+		cursor pointer
+		padding 0
+		margin 8px 4px 0 0
+		width 40px
+		height 40px
+		font-size 1em
+		color rgba($theme-color, 0.5)
+		background transparent
+		outline none
+		border solid 1px transparent
+		border-radius 4px
+
+		&:hover
+			background transparent
+			border-color rgba($theme-color, 0.3)
+
+		&:active
+			color rgba($theme-color, 0.6)
+			background 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
+
+		&:focus
+			&:after
+				content ""
+				pointer-events none
+				position absolute
+				top -5px
+				right -5px
+				bottom -5px
+				left -5px
+				border 2px solid rgba($theme-color, 0.3)
+				border-radius 8px
+
+	> .dropzone
+		position absolute
+		left 0
+		top 0
+		width 100%
+		height 100%
+		border dashed 2px rgba($theme-color, 0.5)
+		pointer-events none
+
+</style>

From f35f94ef8f02b095eb59ad34b970d21359554cc3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 19:02:19 +0900
Subject: [PATCH 055/286] wip

---
 src/web/app/desktop/-tags/repost-form.tag     | 127 -----------------
 .../desktop/views/components/repost-form.vue  | 131 ++++++++++++++++++
 2 files changed, 131 insertions(+), 127 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/repost-form.tag
 create mode 100644 src/web/app/desktop/views/components/repost-form.vue

diff --git a/src/web/app/desktop/-tags/repost-form.tag b/src/web/app/desktop/-tags/repost-form.tag
deleted file mode 100644
index afe555b6d..000000000
--- a/src/web/app/desktop/-tags/repost-form.tag
+++ /dev/null
@@ -1,127 +0,0 @@
-<mk-repost-form>
-	<mk-post-preview post={ opts.post }/>
-	<template v-if="!quote">
-		<footer>
-			<a class="quote" v-if="!quote" @click="onquote">%i18n:desktop.tags.mk-repost-form.quote%</a>
-			<button class="cancel" @click="cancel">%i18n:desktop.tags.mk-repost-form.cancel%</button>
-			<button class="ok" @click="ok" disabled={ wait }>{ wait ? '%i18n:desktop.tags.mk-repost-form.reposting%' : '%i18n:desktop.tags.mk-repost-form.repost%' }</button>
-		</footer>
-	</template>
-	<template v-if="quote">
-		<mk-post-form ref="form" repost={ opts.post }/>
-	</template>
-	<style lang="stylus" scoped>
-		:scope
-
-			> mk-post-preview
-				margin 16px 22px
-
-			> div
-				padding 16px
-
-			> footer
-				height 72px
-				background lighten($theme-color, 95%)
-
-				> .quote
-					position absolute
-					bottom 16px
-					left 28px
-					line-height 40px
-
-				button
-					display block
-					position absolute
-					bottom 16px
-					cursor pointer
-					padding 0
-					margin 0
-					width 120px
-					height 40px
-					font-size 1em
-					outline none
-					border-radius 4px
-
-					&:focus
-						&:after
-							content ""
-							pointer-events none
-							position absolute
-							top -5px
-							right -5px
-							bottom -5px
-							left -5px
-							border 2px solid rgba($theme-color, 0.3)
-							border-radius 8px
-
-				> .cancel
-					right 148px
-					color #888
-					background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
-					border solid 1px #e2e2e2
-
-					&:hover
-						background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
-						border-color #dcdcdc
-
-					&:active
-						background #ececec
-						border-color #dcdcdc
-
-				> .ok
-					right 16px
-					font-weight bold
-					color $theme-color-foreground
-					background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
-					border solid 1px lighten($theme-color, 15%)
-
-					&:hover
-						background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
-						border-color $theme-color
-
-					&:active
-						background $theme-color
-						border-color $theme-color
-
-	</style>
-	<script lang="typescript">
-		import notify from '../scripts/notify';
-
-		this.mixin('api');
-
-		this.wait = false;
-		this.quote = false;
-
-		this.cancel = () => {
-			this.$emit('cancel');
-		};
-
-		this.ok = () => {
-			this.wait = true;
-			this.api('posts/create', {
-				repost_id: this.opts.post.id
-			}).then(data => {
-				this.$emit('posted');
-				notify('%i18n:desktop.tags.mk-repost-form.success%');
-			}).catch(err => {
-				notify('%i18n:desktop.tags.mk-repost-form.failure%');
-			}).then(() => {
-				this.update({
-					wait: false
-				});
-			});
-		};
-
-		this.onquote = () => {
-			this.update({
-				quote: true
-			});
-
-			this.$refs.form.on('post', () => {
-				this.$emit('posted');
-			});
-
-			this.$refs.form.focus();
-		};
-	</script>
-</mk-repost-form>
diff --git a/src/web/app/desktop/views/components/repost-form.vue b/src/web/app/desktop/views/components/repost-form.vue
new file mode 100644
index 000000000..9e9f7174f
--- /dev/null
+++ b/src/web/app/desktop/views/components/repost-form.vue
@@ -0,0 +1,131 @@
+<template>
+<div class="mk-repost-form">
+	<mk-post-preview :post="post"/>
+	<template v-if="!quote">
+		<footer>
+			<a class="quote" v-if="!quote" @click="onquote">%i18n:desktop.tags.mk-repost-form.quote%</a>
+			<button class="cancel" @click="cancel">%i18n:desktop.tags.mk-repost-form.cancel%</button>
+			<button class="ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:desktop.tags.mk-repost-form.reposting%' : '%i18n:desktop.tags.mk-repost-form.repost%' }}</button>
+		</footer>
+	</template>
+	<template v-if="quote">
+		<mk-post-form ref="form" :repost="post" @posted="onChildFormPosted"/>
+	</template>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import notify from '../../scripts/notify';
+
+export default Vue.extend({
+	props: ['post'],
+	data() {
+		return {
+			wait: false,
+			quote: false
+		};
+	},
+	methods: {
+		ok() {
+			this.wait = true;
+			this.$root.$data.os.api('posts/create', {
+				repost_id: this.post.id
+			}).then(data => {
+				this.$emit('posted');
+				notify('%i18n:desktop.tags.mk-repost-form.success%');
+			}).catch(err => {
+				notify('%i18n:desktop.tags.mk-repost-form.failure%');
+			}).then(() => {
+				this.wait = false;
+			});
+		},
+		cancel() {
+			this.$emit('canceled');
+		},
+		onQuote() {
+			this.quote = true;
+
+			(this.$refs.form as any).focus();
+		},
+		onChildFormPosted() {
+			this.$emit('posted');
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-repost-form
+
+	> mk-post-preview
+		margin 16px 22px
+
+	> div
+		padding 16px
+
+	> footer
+		height 72px
+		background lighten($theme-color, 95%)
+
+		> .quote
+			position absolute
+			bottom 16px
+			left 28px
+			line-height 40px
+
+		button
+			display block
+			position absolute
+			bottom 16px
+			cursor pointer
+			padding 0
+			margin 0
+			width 120px
+			height 40px
+			font-size 1em
+			outline none
+			border-radius 4px
+
+			&:focus
+				&:after
+					content ""
+					pointer-events none
+					position absolute
+					top -5px
+					right -5px
+					bottom -5px
+					left -5px
+					border 2px solid rgba($theme-color, 0.3)
+					border-radius 8px
+
+		> .cancel
+			right 148px
+			color #888
+			background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
+			border solid 1px #e2e2e2
+
+			&:hover
+				background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
+				border-color #dcdcdc
+
+			&:active
+				background #ececec
+				border-color #dcdcdc
+
+		> .ok
+			right 16px
+			font-weight bold
+			color $theme-color-foreground
+			background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+			border solid 1px lighten($theme-color, 15%)
+
+			&:hover
+				background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
+				border-color $theme-color
+
+			&:active
+				background $theme-color
+				border-color $theme-color
+
+</style>

From c0aec849c3d8c2a545b644f2c346bf61ec9072d8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 19:17:47 +0900
Subject: [PATCH 056/286] wip

---
 src/web/app/desktop/-tags/post-preview.tag    |  94 ---------------
 .../desktop/views/components/post-preview.vue | 108 ++++++++++++++++++
 2 files changed, 108 insertions(+), 94 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/post-preview.tag
 create mode 100644 src/web/app/desktop/views/components/post-preview.vue

diff --git a/src/web/app/desktop/-tags/post-preview.tag b/src/web/app/desktop/-tags/post-preview.tag
deleted file mode 100644
index eb71e5e87..000000000
--- a/src/web/app/desktop/-tags/post-preview.tag
+++ /dev/null
@@ -1,94 +0,0 @@
-<mk-post-preview title={ title }>
-	<article><a class="avatar-anchor" href={ '/' + post.user.username }><img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar" data-user-preview={ post.user_id }/></a>
-		<div class="main">
-			<header><a class="name" href={ '/' + post.user.username } data-user-preview={ post.user_id }>{ post.user.name }</a><span class="username">@{ post.user.username }</span><a class="time" href={ '/' + post.user.username + '/' + post.id }>
-					<mk-time time={ post.created_at }/></a></header>
-			<div class="body">
-				<mk-sub-post-content class="text" post={ post }/>
-			</div>
-		</div>
-	</article>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 0
-			font-size 0.9em
-			background #fff
-
-			> article
-
-				&:after
-					content ""
-					display block
-					clear both
-
-				&:hover
-					> .main > footer > button
-						color #888
-
-				> .avatar-anchor
-					display block
-					float left
-					margin 0 16px 0 0
-
-					> .avatar
-						display block
-						width 52px
-						height 52px
-						margin 0
-						border-radius 8px
-						vertical-align bottom
-
-				> .main
-					float left
-					width calc(100% - 68px)
-
-					> header
-						display flex
-						margin 4px 0
-						white-space nowrap
-
-						> .name
-							margin 0 .5em 0 0
-							padding 0
-							color #607073
-							font-size 1em
-							line-height 1.1em
-							font-weight 700
-							text-align left
-							text-decoration none
-							white-space normal
-
-							&:hover
-								text-decoration underline
-
-						> .username
-							text-align left
-							margin 0 .5em 0 0
-							color #d1d8da
-
-						> .time
-							margin-left auto
-							color #b2b8bb
-
-					> .body
-
-						> .text
-							cursor default
-							margin 0
-							padding 0
-							font-size 1.1em
-							color #717171
-
-	</style>
-	<script lang="typescript">
-		import dateStringify from '../../common/scripts/date-stringify';
-
-		this.mixin('user-preview');
-
-		this.post = this.opts.post;
-
-		this.title = dateStringify(this.post.created_at);
-	</script>
-</mk-post-preview>
diff --git a/src/web/app/desktop/views/components/post-preview.vue b/src/web/app/desktop/views/components/post-preview.vue
new file mode 100644
index 000000000..fc297dccc
--- /dev/null
+++ b/src/web/app/desktop/views/components/post-preview.vue
@@ -0,0 +1,108 @@
+<template>
+<div class="mk-post-preview" :title="title">
+	<a class="avatar-anchor" :href="`/${post.user.username}`">
+		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" :v-user-preview="post.user_id"/>
+	</a>
+	<div class="main">
+		<header>
+			<a class="name" :href="`/${post.user.username}`" :v-user-preview="post.user_id">{{ post.user.name }}</a>
+			<span class="username">@{ post.user.username }</span>
+			<a class="time" :href="`/${post.user.username}/${post.id}`">
+			<mk-time :time="post.created_at"/></a>
+		</header>
+		<div class="body">
+			<mk-sub-post-content class="text" :post="post"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../../common/scripts/date-stringify';
+
+export default Vue.extend({
+	props: ['post'],
+	computed: {
+		title(): string {
+			return dateStringify(this.post.created_at);
+		}
+	}
+});
+</script>
+
+
+<style lang="stylus" scoped>
+.mk-post-preview
+	display block
+	margin 0
+	padding 0
+	font-size 0.9em
+	background #fff
+
+	> article
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		&:hover
+			> .main > footer > button
+				color #888
+
+		> .avatar-anchor
+			display block
+			float left
+			margin 0 16px 0 0
+
+			> .avatar
+				display block
+				width 52px
+				height 52px
+				margin 0
+				border-radius 8px
+				vertical-align bottom
+
+		> .main
+			float left
+			width calc(100% - 68px)
+
+			> header
+				display flex
+				margin 4px 0
+				white-space nowrap
+
+				> .name
+					margin 0 .5em 0 0
+					padding 0
+					color #607073
+					font-size 1em
+					line-height 1.1em
+					font-weight 700
+					text-align left
+					text-decoration none
+					white-space normal
+
+					&:hover
+						text-decoration underline
+
+				> .username
+					text-align left
+					margin 0 .5em 0 0
+					color #d1d8da
+
+				> .time
+					margin-left auto
+					color #b2b8bb
+
+			> .body
+
+				> .text
+					cursor default
+					margin 0
+					padding 0
+					font-size 1.1em
+					color #717171
+
+</style>

From 61838246b3b281c571f6a9b25002e1a3b5f31af4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 19:59:24 +0900
Subject: [PATCH 057/286] wip

---
 src/web/app/desktop/-tags/ui.tag            | 37 ---------------------
 src/web/app/desktop/views/components/ui.vue | 35 +++++++++++++++++--
 2 files changed, 33 insertions(+), 39 deletions(-)

diff --git a/src/web/app/desktop/-tags/ui.tag b/src/web/app/desktop/-tags/ui.tag
index e5008b838..f8b7b3f4f 100644
--- a/src/web/app/desktop/-tags/ui.tag
+++ b/src/web/app/desktop/-tags/ui.tag
@@ -1,40 +1,3 @@
-<mk-ui>
-	<mk-ui-header page={ opts.page }/>
-	<mk-set-avatar-suggestion v-if="SIGNIN && I.avatar_id == null"/>
-	<mk-set-banner-suggestion v-if="SIGNIN && I.banner_id == null"/>
-	<div class="content">
-		<yield />
-	</div>
-	<mk-stream-indicator v-if="SIGNIN"/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-
-		this.openPostForm = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-post-form-window')));
-		};
-
-		this.on('mount', () => {
-			document.addEventListener('keydown', this.onkeydown);
-		});
-
-		this.on('unmount', () => {
-			document.removeEventListener('keydown', this.onkeydown);
-		});
-
-		this.onkeydown = e => {
-			if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return;
-
-			if (e.which == 80 || e.which == 78) { // p or n
-				e.preventDefault();
-				this.openPostForm();
-			}
-		};
-	</script>
-</mk-ui>
 
 <mk-ui-header>
 	<mk-donation v-if="SIGNIN && I.client_settings.show_donation"/>
diff --git a/src/web/app/desktop/views/components/ui.vue b/src/web/app/desktop/views/components/ui.vue
index 34ac86f70..39ec057f8 100644
--- a/src/web/app/desktop/views/components/ui.vue
+++ b/src/web/app/desktop/views/components/ui.vue
@@ -1,6 +1,37 @@
 <template>
 <div>
-	<header>misskey</header>
-	<slot></slot>
+	<mk-ui-header/>
+	<div class="content">
+		<slot></slot>
+	</div>
+	<mk-stream-indicator v-if="$root.$data.os.isSignedIn"/>
 </div>
 </template>
+
+<script lang="ts">
+import Vue from 'vue';
+import MkPostFormWindow from './post-form-window.vue';
+
+export default Vue.extend({
+	mounted() {
+		document.addEventListener('keydown', this.onKeydown);
+	},
+	beforeDestroy() {
+		document.removeEventListener('keydown', this.onKeydown);
+	},
+	methods: {
+		openPostForm() {
+			document.body.appendChild(new MkPostFormWindow().$mount().$el);
+		},
+		onKeydown(e) {
+			if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return;
+
+			if (e.which == 80 || e.which == 78) { // p or n
+				e.preventDefault();
+				this.openPostForm();
+			}
+		}
+	}
+});
+</script>
+

From dd724b609f892f12aa5ca5fe7f6edde420a70151 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 21:10:16 +0900
Subject: [PATCH 058/286] wip

---
 src/web/app/desktop/-tags/donation.tag        |  66 --
 src/web/app/desktop/-tags/ui.tag              | 859 ------------------
 .../views/components/ui-header-account.vue    | 210 +++++
 .../views/components/ui-header-clock.vue      | 109 +++
 .../views/components/ui-header-nav.vue        | 151 +++
 .../components/ui-header-notifications.vue    | 156 ++++
 .../components/ui-header-post-button.vue      |  52 ++
 .../views/components/ui-header-search.vue     |  68 ++
 .../desktop/views/components/ui-header.vue    |  86 ++
 .../views/components/ui-notification.vue      |  59 ++
 10 files changed, 891 insertions(+), 925 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/donation.tag
 delete mode 100644 src/web/app/desktop/-tags/ui.tag
 create mode 100644 src/web/app/desktop/views/components/ui-header-account.vue
 create mode 100644 src/web/app/desktop/views/components/ui-header-clock.vue
 create mode 100644 src/web/app/desktop/views/components/ui-header-nav.vue
 create mode 100644 src/web/app/desktop/views/components/ui-header-notifications.vue
 create mode 100644 src/web/app/desktop/views/components/ui-header-post-button.vue
 create mode 100644 src/web/app/desktop/views/components/ui-header-search.vue
 create mode 100644 src/web/app/desktop/views/components/ui-header.vue
 create mode 100644 src/web/app/desktop/views/components/ui-notification.vue

diff --git a/src/web/app/desktop/-tags/donation.tag b/src/web/app/desktop/-tags/donation.tag
deleted file mode 100644
index fe446f2e6..000000000
--- a/src/web/app/desktop/-tags/donation.tag
+++ /dev/null
@@ -1,66 +0,0 @@
-<mk-donation>
-	<button class="close" @click="close">閉じる x</button>
-	<div class="message">
-		<p>利用者の皆さま、</p>
-		<p>
-			今日は、日本の皆さまにお知らせがあります。
-			Misskeyの援助をお願いいたします。
-			私は独立性を守るため、一切の広告を掲載いたしません。
-			平均で約¥1,500の寄付をいただき、運営しております。
-			援助をしてくださる利用者はほんの少数です。
-			お願いいたします。
-			今日、利用者の皆さまが¥300ご援助くだされば、募金活動を一時間で終了することができます。
-			コーヒー1杯ほどの金額です。
-			Misskeyを活用しておられるのでしたら、広告を掲載せずにもう1年活動できるよう、どうか1分だけお時間をください。
-			私は小さな非営利個人ですが、サーバー、プログラム、人件費など、世界でトップクラスのウェブサイト同等のコストがかかります。
-			利用者は何億人といますが、他の大きなサイトに比べてほんの少額の費用で運営しているのです。
-			人間の可能性、自由、そして機会。知識こそ、これらの基盤を成すものです。
-			私は、誰もが無料かつ制限なく知識に触れられるべきだと信じています。
-			募金活動を終了し、Misskeyの改善に戻れるようご援助ください。
-			よろしくお願いいたします。
-		</p>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			color #fff
-			background #03072C
-
-			> .close
-				position absolute
-				top 16px
-				right 16px
-				z-index 1
-
-			> .message
-				padding 32px
-				font-size 1.4em
-				font-family serif
-
-				> p
-					display block
-					margin 0 auto
-					max-width 1200px
-
-				> p:first-child
-					margin-bottom 16px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-		this.mixin('api');
-
-		this.close = e => {
-			e.preventDefault();
-			e.stopPropagation();
-
-			this.I.client_settings.show_donation = false;
-			this.I.update();
-			this.api('i/update', {
-				show_donation: false
-			});
-
-			this.$destroy();
-		};
-	</script>
-</mk-donation>
diff --git a/src/web/app/desktop/-tags/ui.tag b/src/web/app/desktop/-tags/ui.tag
deleted file mode 100644
index f8b7b3f4f..000000000
--- a/src/web/app/desktop/-tags/ui.tag
+++ /dev/null
@@ -1,859 +0,0 @@
-
-<mk-ui-header>
-	<mk-donation v-if="SIGNIN && I.client_settings.show_donation"/>
-	<mk-special-message/>
-	<div class="main">
-		<div class="backdrop"></div>
-		<div class="main">
-			<div class="container">
-				<div class="left">
-					<mk-ui-header-nav page={ opts.page }/>
-				</div>
-				<div class="right">
-					<mk-ui-header-search/>
-					<mk-ui-header-account v-if="SIGNIN"/>
-					<mk-ui-header-notifications v-if="SIGNIN"/>
-					<mk-ui-header-post-button v-if="SIGNIN"/>
-					<mk-ui-header-clock/>
-				</div>
-			</div>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			position -webkit-sticky
-			position sticky
-			top 0
-			z-index 1024
-			width 100%
-			box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
-
-			> .main
-
-				> .backdrop
-					position absolute
-					top 0
-					z-index 1023
-					width 100%
-					height 48px
-					backdrop-filter blur(12px)
-					background #f7f7f7
-
-					&:after
-						content ""
-						display block
-						width 100%
-						height 48px
-						background-image url(/assets/desktop/header-logo.svg)
-						background-size 46px
-						background-position center
-						background-repeat no-repeat
-						opacity 0.3
-
-				> .main
-					z-index 1024
-					margin 0
-					padding 0
-					background-clip content-box
-					font-size 0.9rem
-					user-select none
-
-					> .container
-						width 100%
-						max-width 1300px
-						margin 0 auto
-
-						&:after
-							content ""
-							display block
-							clear both
-
-						> .left
-							float left
-							height 3rem
-
-						> .right
-							float right
-							height 48px
-
-							@media (max-width 1100px)
-								> mk-ui-header-search
-									display none
-
-	</style>
-	<script lang="typescript">this.mixin('i');</script>
-</mk-ui-header>
-
-<mk-ui-header-search>
-	<form class="search" onsubmit={ onsubmit }>
-		%fa:search%
-		<input ref="q" type="search" placeholder="%i18n:desktop.tags.mk-ui-header-search.placeholder%"/>
-		<div class="result"></div>
-	</form>
-	<style lang="stylus" scoped>
-		:scope
-
-			> form
-				display block
-				float left
-
-				> [data-fa]
-					display block
-					position absolute
-					top 0
-					left 0
-					width 48px
-					text-align center
-					line-height 48px
-					color #9eaba8
-					pointer-events none
-
-					> *
-						vertical-align middle
-
-				> input
-					user-select text
-					cursor auto
-					margin 8px 0 0 0
-					padding 6px 18px 6px 36px
-					width 14em
-					height 32px
-					font-size 1em
-					background rgba(0, 0, 0, 0.05)
-					outline none
-					//border solid 1px #ddd
-					border none
-					border-radius 16px
-					transition color 0.5s ease, border 0.5s ease
-					font-family FontAwesome, sans-serif
-
-					&::placeholder
-						color #9eaba8
-
-					&:hover
-						background rgba(0, 0, 0, 0.08)
-
-					&:focus
-						box-shadow 0 0 0 2px rgba($theme-color, 0.5) !important
-
-	</style>
-	<script lang="typescript">
-		this.mixin('page');
-
-		this.onsubmit = e => {
-			e.preventDefault();
-			this.page('/search?q=' + encodeURIComponent(this.$refs.q.value));
-		};
-	</script>
-</mk-ui-header-search>
-
-<mk-ui-header-post-button>
-	<button @click="post" title="%i18n:desktop.tags.mk-ui-header-post-button.post%">%fa:pencil-alt%</button>
-	<style lang="stylus" scoped>
-		:scope
-			display inline-block
-			padding 8px
-			height 100%
-			vertical-align top
-
-			> button
-				display inline-block
-				margin 0
-				padding 0 10px
-				height 100%
-				font-size 1.2em
-				font-weight normal
-				text-decoration none
-				color $theme-color-foreground
-				background $theme-color !important
-				outline none
-				border none
-				border-radius 4px
-				transition background 0.1s ease
-				cursor pointer
-
-				*
-					pointer-events none
-
-				&:hover
-					background lighten($theme-color, 10%) !important
-
-				&:active
-					background darken($theme-color, 10%) !important
-					transition background 0s ease
-
-	</style>
-	<script lang="typescript">
-		this.post = e => {
-			this.parent.parent.openPostForm();
-		};
-	</script>
-</mk-ui-header-post-button>
-
-<mk-ui-header-notifications>
-	<button data-active={ isOpen } @click="toggle" title="%i18n:desktop.tags.mk-ui-header-notifications.title%">
-		%fa:R bell%<template v-if="hasUnreadNotifications">%fa:circle%</template>
-	</button>
-	<div class="notifications" v-if="isOpen">
-		<mk-notifications/>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			float left
-
-			> button
-				display block
-				margin 0
-				padding 0
-				width 32px
-				color #9eaba8
-				border none
-				background transparent
-				cursor pointer
-
-				*
-					pointer-events none
-
-				&:hover
-				&[data-active='true']
-					color darken(#9eaba8, 20%)
-
-				&:active
-					color darken(#9eaba8, 30%)
-
-				> [data-fa].bell
-					font-size 1.2em
-					line-height 48px
-
-				> [data-fa].circle
-					margin-left -5px
-					vertical-align super
-					font-size 10px
-					color $theme-color
-
-			> .notifications
-				display block
-				position absolute
-				top 56px
-				right -72px
-				width 300px
-				background #fff
-				border-radius 4px
-				box-shadow 0 1px 4px rgba(0, 0, 0, 0.25)
-
-				&:before
-					content ""
-					pointer-events none
-					display block
-					position absolute
-					top -28px
-					right 74px
-					border-top solid 14px transparent
-					border-right solid 14px transparent
-					border-bottom solid 14px rgba(0, 0, 0, 0.1)
-					border-left solid 14px transparent
-
-				&:after
-					content ""
-					pointer-events none
-					display block
-					position absolute
-					top -27px
-					right 74px
-					border-top solid 14px transparent
-					border-right solid 14px transparent
-					border-bottom solid 14px #fff
-					border-left solid 14px transparent
-
-				> mk-notifications
-					max-height 350px
-					font-size 1rem
-					overflow auto
-
-	</style>
-	<script lang="typescript">
-		import contains from '../../common/scripts/contains';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		if (this.SIGNIN) {
-			this.mixin('stream');
-			this.connection = this.stream.getConnection();
-			this.connectionId = this.stream.use();
-		}
-
-		this.isOpen = false;
-
-		this.on('mount', () => {
-			if (this.SIGNIN) {
-				this.connection.on('read_all_notifications', this.onReadAllNotifications);
-				this.connection.on('unread_notification', this.onUnreadNotification);
-
-				// Fetch count of unread notifications
-				this.api('notifications/get_unread_count').then(res => {
-					if (res.count > 0) {
-						this.update({
-							hasUnreadNotifications: true
-						});
-					}
-				});
-			}
-		});
-
-		this.on('unmount', () => {
-			if (this.SIGNIN) {
-				this.connection.off('read_all_notifications', this.onReadAllNotifications);
-				this.connection.off('unread_notification', this.onUnreadNotification);
-				this.stream.dispose(this.connectionId);
-			}
-		});
-
-		this.onReadAllNotifications = () => {
-			this.update({
-				hasUnreadNotifications: false
-			});
-		};
-
-		this.onUnreadNotification = () => {
-			this.update({
-				hasUnreadNotifications: true
-			});
-		};
-
-		this.toggle = () => {
-			this.isOpen ? this.close() : this.open();
-		};
-
-		this.open = () => {
-			this.update({
-				isOpen: true
-			});
-			document.querySelectorAll('body *').forEach(el => {
-				el.addEventListener('mousedown', this.mousedown);
-			});
-		};
-
-		this.close = () => {
-			this.update({
-				isOpen: false
-			});
-			document.querySelectorAll('body *').forEach(el => {
-				el.removeEventListener('mousedown', this.mousedown);
-			});
-		};
-
-		this.mousedown = e => {
-			e.preventDefault();
-			if (!contains(this.root, e.target) && this.root != e.target) this.close();
-			return false;
-		};
-	</script>
-</mk-ui-header-notifications>
-
-<mk-ui-header-nav>
-	<ul>
-		<template v-if="SIGNIN">
-			<li class="home { active: page == 'home' }">
-				<a href={ _URL_ }>
-					%fa:home%
-					<p>%i18n:desktop.tags.mk-ui-header-nav.home%</p>
-				</a>
-			</li>
-			<li class="messaging">
-				<a @click="messaging">
-					%fa:comments%
-					<p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p>
-					<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>
-				</a>
-			</li>
-		</template>
-		<li class="ch">
-			<a href={ _CH_URL_ } target="_blank">
-				%fa:tv%
-				<p>%i18n:desktop.tags.mk-ui-header-nav.ch%</p>
-			</a>
-		</li>
-		<li class="info">
-			<a href="https://twitter.com/misskey_xyz" target="_blank">
-				%fa:info%
-				<p>%i18n:desktop.tags.mk-ui-header-nav.info%</p>
-			</a>
-		</li>
-	</ul>
-	<style lang="stylus" scoped>
-		:scope
-			display inline-block
-			margin 0
-			padding 0
-			line-height 3rem
-			vertical-align top
-
-			> ul
-				display inline-block
-				margin 0
-				padding 0
-				vertical-align top
-				line-height 3rem
-				list-style none
-
-				> li
-					display inline-block
-					vertical-align top
-					height 48px
-					line-height 48px
-
-					&.active
-						> a
-							border-bottom solid 3px $theme-color
-
-					> a
-						display inline-block
-						z-index 1
-						height 100%
-						padding 0 24px
-						font-size 13px
-						font-variant small-caps
-						color #9eaba8
-						text-decoration none
-						transition none
-						cursor pointer
-
-						*
-							pointer-events none
-
-						&:hover
-							color darken(#9eaba8, 20%)
-							text-decoration none
-
-						> [data-fa]:first-child
-							margin-right 8px
-
-						> [data-fa]:last-child
-							margin-left 5px
-							font-size 10px
-							color $theme-color
-
-							@media (max-width 1100px)
-								margin-left -5px
-
-						> p
-							display inline
-							margin 0
-
-							@media (max-width 1100px)
-								display none
-
-						@media (max-width 700px)
-							padding 0 12px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-		this.mixin('api');
-
-		if (this.SIGNIN) {
-			this.mixin('stream');
-			this.connection = this.stream.getConnection();
-			this.connectionId = this.stream.use();
-		}
-
-		this.page = this.opts.page;
-
-		this.on('mount', () => {
-			if (this.SIGNIN) {
-				this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
-				this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
-
-				// Fetch count of unread messaging messages
-				this.api('messaging/unread').then(res => {
-					if (res.count > 0) {
-						this.update({
-							hasUnreadMessagingMessages: true
-						});
-					}
-				});
-			}
-		});
-
-		this.on('unmount', () => {
-			if (this.SIGNIN) {
-				this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
-				this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
-				this.stream.dispose(this.connectionId);
-			}
-		});
-
-		this.onReadAllMessagingMessages = () => {
-			this.update({
-				hasUnreadMessagingMessages: false
-			});
-		};
-
-		this.onUnreadMessagingMessage = () => {
-			this.update({
-				hasUnreadMessagingMessages: true
-			});
-		};
-
-		this.messaging = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-messaging-window')));
-		};
-	</script>
-</mk-ui-header-nav>
-
-<mk-ui-header-clock>
-	<div class="header">
-		<time ref="time">
-			<span class="yyyymmdd">{ yyyy }/{ mm }/{ dd }</span>
-			<br>
-			<span class="hhnn">{ hh }<span style="visibility:{ now.getSeconds() % 2 == 0 ? 'visible' : 'hidden' }">:</span>{ nn }</span>
-		</time>
-	</div>
-	<div class="content">
-		<mk-analog-clock/>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display inline-block
-			overflow visible
-
-			> .header
-				padding 0 12px
-				text-align center
-				font-size 10px
-
-				&, *
-					cursor: default
-
-				&:hover
-					background #899492
-
-					& + .content
-						visibility visible
-
-					> time
-						color #fff !important
-
-						*
-							color #fff !important
-
-				&:after
-					content ""
-					display block
-					clear both
-
-				> time
-					display table-cell
-					vertical-align middle
-					height 48px
-					color #9eaba8
-
-					> .yyyymmdd
-						opacity 0.7
-
-			> .content
-				visibility hidden
-				display block
-				position absolute
-				top auto
-				right 0
-				z-index 3
-				margin 0
-				padding 0
-				width 256px
-				background #899492
-
-	</style>
-	<script lang="typescript">
-		this.now = new Date();
-
-		this.draw = () => {
-			const now = this.now = new Date();
-			this.yyyy = now.getFullYear();
-			this.mm = ('0' + (now.getMonth() + 1)).slice(-2);
-			this.dd = ('0' + now.getDate()).slice(-2);
-			this.hh = ('0' + now.getHours()).slice(-2);
-			this.nn = ('0' + now.getMinutes()).slice(-2);
-			this.update();
-		};
-
-		this.on('mount', () => {
-			this.draw();
-			this.clock = setInterval(this.draw, 1000);
-		});
-
-		this.on('unmount', () => {
-			clearInterval(this.clock);
-		});
-	</script>
-</mk-ui-header-clock>
-
-<mk-ui-header-account>
-	<button class="header" data-active={ isOpen.toString() } @click="toggle">
-		<span class="username">{ I.username }<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
-		<img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-	</button>
-	<div class="menu" v-if="isOpen">
-		<ul>
-			<li>
-				<a href={ '/' + I.username }>%fa:user%%i18n:desktop.tags.mk-ui-header-account.profile%%fa:angle-right%</a>
-			</li>
-			<li @click="drive">
-				<p>%fa:cloud%%i18n:desktop.tags.mk-ui-header-account.drive%%fa:angle-right%</p>
-			</li>
-			<li>
-				<a href="/i/mentions">%fa:at%%i18n:desktop.tags.mk-ui-header-account.mentions%%fa:angle-right%</a>
-			</li>
-		</ul>
-		<ul>
-			<li @click="settings">
-				<p>%fa:cog%%i18n:desktop.tags.mk-ui-header-account.settings%%fa:angle-right%</p>
-			</li>
-		</ul>
-		<ul>
-			<li @click="signout">
-				<p>%fa:power-off%%i18n:desktop.tags.mk-ui-header-account.signout%%fa:angle-right%</p>
-			</li>
-		</ul>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			float left
-
-			> .header
-				display block
-				margin 0
-				padding 0
-				color #9eaba8
-				border none
-				background transparent
-				cursor pointer
-
-				*
-					pointer-events none
-
-				&:hover
-				&[data-active='true']
-					color darken(#9eaba8, 20%)
-
-					> .avatar
-						filter saturate(150%)
-
-				&:active
-					color darken(#9eaba8, 30%)
-
-				> .username
-					display block
-					float left
-					margin 0 12px 0 16px
-					max-width 16em
-					line-height 48px
-					font-weight bold
-					font-family Meiryo, sans-serif
-					text-decoration none
-
-					[data-fa]
-						margin-left 8px
-
-				> .avatar
-					display block
-					float left
-					min-width 32px
-					max-width 32px
-					min-height 32px
-					max-height 32px
-					margin 8px 8px 8px 0
-					border-radius 4px
-					transition filter 100ms ease
-
-			> .menu
-				display block
-				position absolute
-				top 56px
-				right -2px
-				width 230px
-				font-size 0.8em
-				background #fff
-				border-radius 4px
-				box-shadow 0 1px 4px rgba(0, 0, 0, 0.25)
-
-				&:before
-					content ""
-					pointer-events none
-					display block
-					position absolute
-					top -28px
-					right 12px
-					border-top solid 14px transparent
-					border-right solid 14px transparent
-					border-bottom solid 14px rgba(0, 0, 0, 0.1)
-					border-left solid 14px transparent
-
-				&:after
-					content ""
-					pointer-events none
-					display block
-					position absolute
-					top -27px
-					right 12px
-					border-top solid 14px transparent
-					border-right solid 14px transparent
-					border-bottom solid 14px #fff
-					border-left solid 14px transparent
-
-				ul
-					display block
-					margin 10px 0
-					padding 0
-					list-style none
-
-					& + ul
-						padding-top 10px
-						border-top solid 1px #eee
-
-					> li
-						display block
-						margin 0
-						padding 0
-
-						> a
-						> p
-							display block
-							z-index 1
-							padding 0 28px
-							margin 0
-							line-height 40px
-							color #868C8C
-							cursor pointer
-
-							*
-								pointer-events none
-
-							> [data-fa]:first-of-type
-								margin-right 6px
-
-							> [data-fa]:last-of-type
-								display block
-								position absolute
-								top 0
-								right 8px
-								z-index 1
-								padding 0 20px
-								font-size 1.2em
-								line-height 40px
-
-							&:hover, &:active
-								text-decoration none
-								background $theme-color
-								color $theme-color-foreground
-
-	</style>
-	<script lang="typescript">
-		import contains from '../../common/scripts/contains';
-		import signout from '../../common/scripts/signout';
-		this.signout = signout;
-
-		this.mixin('i');
-
-		this.isOpen = false;
-
-		this.on('before-unmount', () => {
-			this.close();
-		});
-
-		this.toggle = () => {
-			this.isOpen ? this.close() : this.open();
-		};
-
-		this.open = () => {
-			this.update({
-				isOpen: true
-			});
-			document.querySelectorAll('body *').forEach(el => {
-				el.addEventListener('mousedown', this.mousedown);
-			});
-		};
-
-		this.close = () => {
-			this.update({
-				isOpen: false
-			});
-			document.querySelectorAll('body *').forEach(el => {
-				el.removeEventListener('mousedown', this.mousedown);
-			});
-		};
-
-		this.mousedown = e => {
-			e.preventDefault();
-			if (!contains(this.root, e.target) && this.root != e.target) this.close();
-			return false;
-		};
-
-		this.drive = () => {
-			this.close();
-			riot.mount(document.body.appendChild(document.createElement('mk-drive-browser-window')));
-		};
-
-		this.settings = () => {
-			this.close();
-			riot.mount(document.body.appendChild(document.createElement('mk-settings-window')));
-		};
-
-	</script>
-</mk-ui-header-account>
-
-<mk-ui-notification>
-	<p>{ opts.message }</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			position fixed
-			z-index 10000
-			top -128px
-			left 0
-			right 0
-			margin 0 auto
-			padding 128px 0 0 0
-			width 500px
-			color rgba(#000, 0.6)
-			background rgba(#fff, 0.9)
-			border-radius 0 0 8px 8px
-			box-shadow 0 2px 4px rgba(#000, 0.2)
-			transform translateY(-64px)
-			opacity 0
-
-			> p
-				margin 0
-				line-height 64px
-				text-align center
-
-	</style>
-	<script lang="typescript">
-		import anime from 'animejs';
-
-		this.on('mount', () => {
-			anime({
-				targets: this.root,
-				opacity: 1,
-				translateY: [-64, 0],
-				easing: 'easeOutElastic',
-				duration: 500
-			});
-
-			setTimeout(() => {
-				anime({
-					targets: this.root,
-					opacity: 0,
-					translateY: -64,
-					duration: 500,
-					easing: 'easeInElastic',
-					complete: () => this.$destroy()
-				});
-			}, 6000);
-		});
-	</script>
-</mk-ui-notification>
diff --git a/src/web/app/desktop/views/components/ui-header-account.vue b/src/web/app/desktop/views/components/ui-header-account.vue
new file mode 100644
index 000000000..435a0dcaf
--- /dev/null
+++ b/src/web/app/desktop/views/components/ui-header-account.vue
@@ -0,0 +1,210 @@
+<template>
+<div class="mk-ui-header-account">
+	<button class="header" :data-active="isOpen" @click="toggle">
+		<span class="username">{{ $root.$data.os.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
+		<img class="avatar" :src="`${ $root.$data.os.i.avatar_url }?thumbnail&size=64`" alt="avatar"/>
+	</button>
+	<div class="menu" v-if="isOpen">
+		<ul>
+			<li>
+				<a :href="`/${ $root.$data.os.i.username }`">%fa:user%%i18n:desktop.tags.mk-ui-header-account.profile%%fa:angle-right%</a>
+			</li>
+			<li @click="drive">
+				<p>%fa:cloud%%i18n:desktop.tags.mk-ui-header-account.drive%%fa:angle-right%</p>
+			</li>
+			<li>
+				<a href="/i/mentions">%fa:at%%i18n:desktop.tags.mk-ui-header-account.mentions%%fa:angle-right%</a>
+			</li>
+		</ul>
+		<ul>
+			<li @click="settings">
+				<p>%fa:cog%%i18n:desktop.tags.mk-ui-header-account.settings%%fa:angle-right%</p>
+			</li>
+		</ul>
+		<ul>
+			<li @click="signout">
+				<p>%fa:power-off%%i18n:desktop.tags.mk-ui-header-account.signout%%fa:angle-right%</p>
+			</li>
+		</ul>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import contains from '../../../common/scripts/contains';
+import signout from '../../../common/scripts/signout';
+
+export default Vue.extend({
+	data() {
+		return {
+			isOpen: false,
+			signout
+		};
+	},
+	beforeDestroy() {
+		this.close();
+	},
+	methods: {
+		toggle() {
+			this.isOpen ? this.close() : this.open();
+		},
+		open() {
+			this.isOpen = true;
+			Array.from(document.querySelectorAll('body *')).forEach(el => {
+				el.addEventListener('mousedown', this.onMousedown);
+			});
+		},
+		close() {
+			this.isOpen = false;
+			Array.from(document.querySelectorAll('body *')).forEach(el => {
+				el.removeEventListener('mousedown', this.onMousedown);
+			});
+		},
+		onMousedown(e) {
+			e.preventDefault();
+			if (!contains(this.$el, e.target) && this.$el != e.target) this.close();
+			return false;
+		},
+		drive() {
+			this.close();
+			document.body.appendChild(new MkDriveWindow().$mount().$el);
+		},
+		settings() {
+			this.close();
+			document.body.appendChild(new MkSettingsWindow().$mount().$el);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-ui-header-account
+	> .header
+		display block
+		margin 0
+		padding 0
+		color #9eaba8
+		border none
+		background transparent
+		cursor pointer
+
+		*
+			pointer-events none
+
+		&:hover
+		&[data-active='true']
+			color darken(#9eaba8, 20%)
+
+			> .avatar
+				filter saturate(150%)
+
+		&:active
+			color darken(#9eaba8, 30%)
+
+		> .username
+			display block
+			float left
+			margin 0 12px 0 16px
+			max-width 16em
+			line-height 48px
+			font-weight bold
+			font-family Meiryo, sans-serif
+			text-decoration none
+
+			[data-fa]
+				margin-left 8px
+
+		> .avatar
+			display block
+			float left
+			min-width 32px
+			max-width 32px
+			min-height 32px
+			max-height 32px
+			margin 8px 8px 8px 0
+			border-radius 4px
+			transition filter 100ms ease
+
+	> .menu
+		display block
+		position absolute
+		top 56px
+		right -2px
+		width 230px
+		font-size 0.8em
+		background #fff
+		border-radius 4px
+		box-shadow 0 1px 4px rgba(0, 0, 0, 0.25)
+
+		&:before
+			content ""
+			pointer-events none
+			display block
+			position absolute
+			top -28px
+			right 12px
+			border-top solid 14px transparent
+			border-right solid 14px transparent
+			border-bottom solid 14px rgba(0, 0, 0, 0.1)
+			border-left solid 14px transparent
+
+		&:after
+			content ""
+			pointer-events none
+			display block
+			position absolute
+			top -27px
+			right 12px
+			border-top solid 14px transparent
+			border-right solid 14px transparent
+			border-bottom solid 14px #fff
+			border-left solid 14px transparent
+
+		ul
+			display block
+			margin 10px 0
+			padding 0
+			list-style none
+
+			& + ul
+				padding-top 10px
+				border-top solid 1px #eee
+
+			> li
+				display block
+				margin 0
+				padding 0
+
+				> a
+				> p
+					display block
+					z-index 1
+					padding 0 28px
+					margin 0
+					line-height 40px
+					color #868C8C
+					cursor pointer
+
+					*
+						pointer-events none
+
+					> [data-fa]:first-of-type
+						margin-right 6px
+
+					> [data-fa]:last-of-type
+						display block
+						position absolute
+						top 0
+						right 8px
+						z-index 1
+						padding 0 20px
+						font-size 1.2em
+						line-height 40px
+
+					&:hover, &:active
+						text-decoration none
+						background $theme-color
+						color $theme-color-foreground
+
+</style>
diff --git a/src/web/app/desktop/views/components/ui-header-clock.vue b/src/web/app/desktop/views/components/ui-header-clock.vue
new file mode 100644
index 000000000..cfed1e84a
--- /dev/null
+++ b/src/web/app/desktop/views/components/ui-header-clock.vue
@@ -0,0 +1,109 @@
+<template>
+<div class="mk-ui-header-clock">
+	<div class="header">
+		<time ref="time">
+			<span class="yyyymmdd">{{ yyyy }}/{{ mm }}/{{ dd }}</span>
+			<br>
+			<span class="hhnn">{{ hh }}<span :style="{ visibility: now.getSeconds() % 2 == 0 ? 'visible' : 'hidden' }">:</span>{{ nn }}</span>
+		</time>
+	</div>
+	<div class="content">
+		<mk-analog-clock/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	data() {
+		return {
+			now: new Date(),
+			clock: null
+		};
+	},
+	computed: {
+		yyyy(): number {
+			return this.now.getFullYear();
+		},
+		mm(): string {
+			return ('0' + (this.now.getMonth() + 1)).slice(-2);
+		},
+		dd(): string {
+			return ('0' + this.now.getDate()).slice(-2);
+		},
+		hh(): string {
+			return ('0' + this.now.getHours()).slice(-2);
+		},
+		nn(): string {
+			return ('0' + this.now.getMinutes()).slice(-2);
+		}
+	},
+	mounted() {
+		this.tick();
+		this.clock = setInterval(this.tick, 1000);
+	},
+	beforeDestroy() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		tick() {
+			this.now = new Date();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-ui-header-clock
+	display inline-block
+	overflow visible
+
+	> .header
+		padding 0 12px
+		text-align center
+		font-size 10px
+
+		&, *
+			cursor: default
+
+		&:hover
+			background #899492
+
+			& + .content
+				visibility visible
+
+			> time
+				color #fff !important
+
+				*
+					color #fff !important
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		> time
+			display table-cell
+			vertical-align middle
+			height 48px
+			color #9eaba8
+
+			> .yyyymmdd
+				opacity 0.7
+
+	> .content
+		visibility hidden
+		display block
+		position absolute
+		top auto
+		right 0
+		z-index 3
+		margin 0
+		padding 0
+		width 256px
+		background #899492
+
+</style>
diff --git a/src/web/app/desktop/views/components/ui-header-nav.vue b/src/web/app/desktop/views/components/ui-header-nav.vue
new file mode 100644
index 000000000..5295787b9
--- /dev/null
+++ b/src/web/app/desktop/views/components/ui-header-nav.vue
@@ -0,0 +1,151 @@
+<template>
+<div class="mk-ui-header-nav">
+	<ul>
+		<template v-if="$root.$data.os.isSignedIn">
+			<li class="home" :class="{ active: page == 'home' }">
+				<a href="/">
+					%fa:home%
+					<p>%i18n:desktop.tags.mk-ui-header-nav.home%</p>
+				</a>
+			</li>
+			<li class="messaging">
+				<a @click="messaging">
+					%fa:comments%
+					<p>%i18n:desktop.tags.mk-ui-header-nav.messaging%</p>
+					<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>
+				</a>
+			</li>
+		</template>
+		<li class="ch">
+			<a :href="_CH_URL_" target="_blank">
+				%fa:tv%
+				<p>%i18n:desktop.tags.mk-ui-header-nav.ch%</p>
+			</a>
+		</li>
+		<li class="info">
+			<a href="https://twitter.com/misskey_xyz" target="_blank">
+				%fa:info%
+				<p>%i18n:desktop.tags.mk-ui-header-nav.info%</p>
+			</a>
+		</li>
+	</ul>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	data() {
+		return {
+			hasUnreadMessagingMessages: false,
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		if (this.$root.$data.os.isSignedIn) {
+			this.connection = this.$root.$data.os.stream.getConnection();
+			this.connectionId = this.$root.$data.os.stream.use();
+
+			this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
+
+			// Fetch count of unread messaging messages
+			this.$root.$data.os.api('messaging/unread').then(res => {
+				if (res.count > 0) {
+					this.hasUnreadMessagingMessages = true;
+				}
+			});
+		}
+	},
+	beforeDestroy() {
+		if (this.$root.$data.os.isSignedIn) {
+			this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
+			this.$root.$data.os.stream.dispose(this.connectionId);
+		}
+	},
+	methods: {
+		onReadAllMessagingMessages() {
+			this.hasUnreadMessagingMessages = false;
+		},
+
+		onUnreadMessagingMessage() {
+			this.hasUnreadMessagingMessages = true;
+		},
+
+		messaging() {
+			document.body.appendChild(new MkMessagingWindow().$mount().$el);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-ui-header-nav
+	display inline-block
+	margin 0
+	padding 0
+	line-height 3rem
+	vertical-align top
+
+	> ul
+		display inline-block
+		margin 0
+		padding 0
+		vertical-align top
+		line-height 3rem
+		list-style none
+
+		> li
+			display inline-block
+			vertical-align top
+			height 48px
+			line-height 48px
+
+			&.active
+				> a
+					border-bottom solid 3px $theme-color
+
+			> a
+				display inline-block
+				z-index 1
+				height 100%
+				padding 0 24px
+				font-size 13px
+				font-variant small-caps
+				color #9eaba8
+				text-decoration none
+				transition none
+				cursor pointer
+
+				*
+					pointer-events none
+
+				&:hover
+					color darken(#9eaba8, 20%)
+					text-decoration none
+
+				> [data-fa]:first-child
+					margin-right 8px
+
+				> [data-fa]:last-child
+					margin-left 5px
+					font-size 10px
+					color $theme-color
+
+					@media (max-width 1100px)
+						margin-left -5px
+
+				> p
+					display inline
+					margin 0
+
+					@media (max-width 1100px)
+						display none
+
+				@media (max-width 700px)
+					padding 0 12px
+
+</style>
diff --git a/src/web/app/desktop/views/components/ui-header-notifications.vue b/src/web/app/desktop/views/components/ui-header-notifications.vue
new file mode 100644
index 000000000..779ee4886
--- /dev/null
+++ b/src/web/app/desktop/views/components/ui-header-notifications.vue
@@ -0,0 +1,156 @@
+<template>
+<div class="mk-ui-header-notifications">
+	<button :data-active="isOpen" @click="toggle" title="%i18n:desktop.tags.mk-ui-header-notifications.title%">
+		%fa:R bell%<template v-if="hasUnreadNotifications">%fa:circle%</template>
+	</button>
+	<div class="notifications" v-if="isOpen">
+		<mk-notifications/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import contains from '../../../common/scripts/contains';
+
+export default Vue.extend({
+	data() {
+		return {
+			isOpen: false,
+			hasUnreadNotifications: false,
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		if (this.$root.$data.os.isSignedIn) {
+			this.connection = this.$root.$data.os.stream.getConnection();
+			this.connectionId = this.$root.$data.os.stream.use();
+
+			this.connection.on('read_all_notifications', this.onReadAllNotifications);
+			this.connection.on('unread_notification', this.onUnreadNotification);
+
+			// Fetch count of unread notifications
+			this.$root.$data.os.api('notifications/get_unread_count').then(res => {
+				if (res.count > 0) {
+					this.hasUnreadNotifications = true;
+				}
+			});
+		}
+	},
+	beforeDestroy() {
+		if (this.$root.$data.os.isSignedIn) {
+			this.connection.off('read_all_notifications', this.onReadAllNotifications);
+			this.connection.off('unread_notification', this.onUnreadNotification);
+			this.$root.$data.os.stream.dispose(this.connectionId);
+		}
+	},
+	methods: {
+		onReadAllNotifications() {
+			this.hasUnreadNotifications = false;
+		},
+
+		onUnreadNotification() {
+			this.hasUnreadNotifications = true;
+		},
+
+		toggle() {
+			this.isOpen ? this.close() : this.open();
+		},
+
+		open() {
+			this.isOpen = true;
+			Array.from(document.querySelectorAll('body *')).forEach(el => {
+				el.addEventListener('mousedown', this.onMousedown);
+			});
+		},
+
+		close() {
+			this.isOpen = false;
+			Array.from(document.querySelectorAll('body *')).forEach(el => {
+				el.removeEventListener('mousedown', this.onMousedown);
+			});
+		},
+
+		onMousedown(e) {
+			e.preventDefault();
+			if (!contains(this.$el, e.target) && this.$el != e.target) this.close();
+			return false;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-ui-header-notifications
+
+	> button
+		display block
+		margin 0
+		padding 0
+		width 32px
+		color #9eaba8
+		border none
+		background transparent
+		cursor pointer
+
+		*
+			pointer-events none
+
+		&:hover
+		&[data-active='true']
+			color darken(#9eaba8, 20%)
+
+		&:active
+			color darken(#9eaba8, 30%)
+
+		> [data-fa].bell
+			font-size 1.2em
+			line-height 48px
+
+		> [data-fa].circle
+			margin-left -5px
+			vertical-align super
+			font-size 10px
+			color $theme-color
+
+	> .notifications
+		display block
+		position absolute
+		top 56px
+		right -72px
+		width 300px
+		background #fff
+		border-radius 4px
+		box-shadow 0 1px 4px rgba(0, 0, 0, 0.25)
+
+		&:before
+			content ""
+			pointer-events none
+			display block
+			position absolute
+			top -28px
+			right 74px
+			border-top solid 14px transparent
+			border-right solid 14px transparent
+			border-bottom solid 14px rgba(0, 0, 0, 0.1)
+			border-left solid 14px transparent
+
+		&:after
+			content ""
+			pointer-events none
+			display block
+			position absolute
+			top -27px
+			right 74px
+			border-top solid 14px transparent
+			border-right solid 14px transparent
+			border-bottom solid 14px #fff
+			border-left solid 14px transparent
+
+		> mk-notifications
+			max-height 350px
+			font-size 1rem
+			overflow auto
+
+</style>
diff --git a/src/web/app/desktop/views/components/ui-header-post-button.vue b/src/web/app/desktop/views/components/ui-header-post-button.vue
new file mode 100644
index 000000000..754e05b23
--- /dev/null
+++ b/src/web/app/desktop/views/components/ui-header-post-button.vue
@@ -0,0 +1,52 @@
+<template>
+<div class="mk-ui-header-post-button">
+	<button @click="post" title="%i18n:desktop.tags.mk-ui-header-post-button.post%">%fa:pencil-alt%</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	methods: {
+		post() {
+			(this.$parent.$parent as any).openPostForm();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-ui-header-post-button
+	display inline-block
+	padding 8px
+	height 100%
+	vertical-align top
+
+	> button
+		display inline-block
+		margin 0
+		padding 0 10px
+		height 100%
+		font-size 1.2em
+		font-weight normal
+		text-decoration none
+		color $theme-color-foreground
+		background $theme-color !important
+		outline none
+		border none
+		border-radius 4px
+		transition background 0.1s ease
+		cursor pointer
+
+		*
+			pointer-events none
+
+		&:hover
+			background lighten($theme-color, 10%) !important
+
+		&:active
+			background darken($theme-color, 10%) !important
+			transition background 0s ease
+
+</style>
diff --git a/src/web/app/desktop/views/components/ui-header-search.vue b/src/web/app/desktop/views/components/ui-header-search.vue
new file mode 100644
index 000000000..a9cddd8ae
--- /dev/null
+++ b/src/web/app/desktop/views/components/ui-header-search.vue
@@ -0,0 +1,68 @@
+<template>
+<form class="ui-header-search" @submit.prevent="onSubmit">
+	%fa:search%
+	<input v-model="q" type="search" placeholder="%i18n:desktop.tags.mk-ui-header-search.placeholder%"/>
+	<div class="result"></div>
+</form>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	data() {
+		return {
+			q: ''
+		};
+	},
+	methods: {
+		onSubmit() {
+			location.href = `/search?q=${encodeURIComponent(this.q)}`;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-ui-header-search
+
+	> [data-fa]
+		display block
+		position absolute
+		top 0
+		left 0
+		width 48px
+		text-align center
+		line-height 48px
+		color #9eaba8
+		pointer-events none
+
+		> *
+			vertical-align middle
+
+	> input
+		user-select text
+		cursor auto
+		margin 8px 0 0 0
+		padding 6px 18px 6px 36px
+		width 14em
+		height 32px
+		font-size 1em
+		background rgba(0, 0, 0, 0.05)
+		outline none
+		//border solid 1px #ddd
+		border none
+		border-radius 16px
+		transition color 0.5s ease, border 0.5s ease
+		font-family FontAwesome, sans-serif
+
+		&::placeholder
+			color #9eaba8
+
+		&:hover
+			background rgba(0, 0, 0, 0.08)
+
+		&:focus
+			box-shadow 0 0 0 2px rgba($theme-color, 0.5) !important
+
+</style>
diff --git a/src/web/app/desktop/views/components/ui-header.vue b/src/web/app/desktop/views/components/ui-header.vue
new file mode 100644
index 000000000..19e4fe697
--- /dev/null
+++ b/src/web/app/desktop/views/components/ui-header.vue
@@ -0,0 +1,86 @@
+<template>
+<div class="mk-ui-header">
+	<mk-special-message/>
+	<div class="main">
+		<div class="backdrop"></div>
+		<div class="main">
+			<div class="container">
+				<div class="left">
+					<mk-ui-header-nav/>
+				</div>
+				<div class="right">
+					<mk-ui-header-search/>
+					<mk-ui-header-account v-if="$root.$data.os.isSignedIn"/>
+					<mk-ui-header-notifications v-if="$root.$data.os.isSignedIn"/>
+					<mk-ui-header-post-button v-if="$root.$data.os.isSignedIn"/>
+					<mk-ui-header-clock/>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<style lang="stylus" scoped>
+.mk-ui-header
+	display block
+	position -webkit-sticky
+	position sticky
+	top 0
+	z-index 1024
+	width 100%
+	box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
+
+	> .main
+
+		> .backdrop
+			position absolute
+			top 0
+			z-index 1023
+			width 100%
+			height 48px
+			backdrop-filter blur(12px)
+			background #f7f7f7
+
+			&:after
+				content ""
+				display block
+				width 100%
+				height 48px
+				background-image url(/assets/desktop/header-logo.svg)
+				background-size 46px
+				background-position center
+				background-repeat no-repeat
+				opacity 0.3
+
+		> .main
+			z-index 1024
+			margin 0
+			padding 0
+			background-clip content-box
+			font-size 0.9rem
+			user-select none
+
+			> .container
+				width 100%
+				max-width 1300px
+				margin 0 auto
+
+				&:after
+					content ""
+					display block
+					clear both
+
+				> .left
+					float left
+					height 3rem
+
+				> .right
+					float right
+					height 48px
+
+					@media (max-width 1100px)
+						> mk-ui-header-search
+							display none
+
+</style>
diff --git a/src/web/app/desktop/views/components/ui-notification.vue b/src/web/app/desktop/views/components/ui-notification.vue
new file mode 100644
index 000000000..f240037d0
--- /dev/null
+++ b/src/web/app/desktop/views/components/ui-notification.vue
@@ -0,0 +1,59 @@
+<template>
+<div class="mk-ui-notification">
+	<p>{{ message }}</p>
+<div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import anime from 'animejs';
+
+export default Vue.extend({
+	props: ['message'],
+	mounted() {
+		anime({
+			targets: this.$el,
+			opacity: 1,
+			translateY: [-64, 0],
+			easing: 'easeOutElastic',
+			duration: 500
+		});
+
+		setTimeout(() => {
+			anime({
+				targets: this.$el,
+				opacity: 0,
+				translateY: -64,
+				duration: 500,
+				easing: 'easeInElastic',
+				complete: () => this.$destroy()
+			});
+		}, 6000);
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-ui-notification
+	display block
+	position fixed
+	z-index 10000
+	top -128px
+	left 0
+	right 0
+	margin 0 auto
+	padding 128px 0 0 0
+	width 500px
+	color rgba(#000, 0.6)
+	background rgba(#fff, 0.9)
+	border-radius 0 0 8px 8px
+	box-shadow 0 2px 4px rgba(#000, 0.2)
+	transform translateY(-64px)
+	opacity 0
+
+	> p
+		margin 0
+		line-height 64px
+		text-align center
+
+</style>

From d3a0546390109922a22c17220d2bde53de333bc2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 22:07:28 +0900
Subject: [PATCH 059/286] wip

---
 .../components/analog-clock.vue}              | 73 +++++++++++--------
 1 file changed, 43 insertions(+), 30 deletions(-)
 rename src/web/app/desktop/{-tags/analog-clock.tag => views/components/analog-clock.vue} (74%)

diff --git a/src/web/app/desktop/-tags/analog-clock.tag b/src/web/app/desktop/views/components/analog-clock.vue
similarity index 74%
rename from src/web/app/desktop/-tags/analog-clock.tag
rename to src/web/app/desktop/views/components/analog-clock.vue
index 6b2bce3b2..a45bafda6 100644
--- a/src/web/app/desktop/-tags/analog-clock.tag
+++ b/src/web/app/desktop/views/components/analog-clock.vue
@@ -1,36 +1,41 @@
-<mk-analog-clock>
-	<canvas ref="canvas" width="256" height="256"></canvas>
-	<style lang="stylus" scoped>
-		:scope
-			> canvas
-				display block
-				width 256px
-				height 256px
-	</style>
-	<script lang="typescript">
-		const Vec2 = function(x, y) {
-			this.x = x;
-			this.y = y;
+<template>
+<canvas class="mk-analog-clock" ref="canvas" width="256" height="256"></canvas>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { themeColor } from '../../../config';
+
+const Vec2 = function(x, y) {
+	this.x = x;
+	this.y = y;
+};
+
+export default Vue.extend({
+	data() {
+		return {
+			clock: null
 		};
+	},
+	mounted() {
+		this.tick();
+		this.clock = setInterval(this.tick, 1000);
+	},
+	beforeDestroy() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		tick() {
+			const canv = this.$refs.canvas as any;
 
-		this.on('mount', () => {
-			this.draw()
-			this.clock = setInterval(this.draw, 1000);
-		});
-
-		this.on('unmount', () => {
-			clearInterval(this.clock);
-		});
-
-		this.draw = () => {
 			const now = new Date();
 			const s = now.getSeconds();
 			const m = now.getMinutes();
 			const h = now.getHours();
 
-			const ctx = this.$refs.canvas.getContext('2d');
-			const canvW = this.$refs.canvas.width;
-			const canvH = this.$refs.canvas.height;
+			const ctx = canv.getContext('2d');
+			const canvW = canv.width;
+			const canvH = canv.height;
 			ctx.clearRect(0, 0, canvW, canvH);
 
 			{ // 背景
@@ -72,7 +77,7 @@
 				const length = Math.min(canvW, canvH) / 4;
 				const uv = new Vec2(Math.sin(angle), -Math.cos(angle));
 				ctx.beginPath();
-				ctx.strokeStyle = _THEME_COLOR_;
+				ctx.strokeStyle = themeColor;
 				ctx.lineWidth = 2;
 				ctx.moveTo(canvW / 2 - uv.x * length / 5, canvH / 2 - uv.y * length / 5);
 				ctx.lineTo(canvW / 2 + uv.x * length,     canvH / 2 + uv.y * length);
@@ -90,6 +95,14 @@
 				ctx.lineTo(canvW / 2 + uv.x * length,     canvH / 2 + uv.y * length);
 				ctx.stroke();
 			}
-		};
-	</script>
-</mk-analog-clock>
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-analog-clock
+	display block
+	width 256px
+	height 256px
+</style>

From ff6ca604f7d6b4125a915e4190efcbbfb0dd42b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 22:18:13 +0900
Subject: [PATCH 060/286] wip

---
 src/web/app/desktop/-tags/settings.tag        | 129 -----------------
 .../app/desktop/views/components/settings.vue | 136 ++++++++++++++++++
 2 files changed, 136 insertions(+), 129 deletions(-)
 create mode 100644 src/web/app/desktop/views/components/settings.vue

diff --git a/src/web/app/desktop/-tags/settings.tag b/src/web/app/desktop/-tags/settings.tag
index 4bf210cef..f4e2910d8 100644
--- a/src/web/app/desktop/-tags/settings.tag
+++ b/src/web/app/desktop/-tags/settings.tag
@@ -1,132 +1,3 @@
-<mk-settings>
-	<div class="nav">
-		<p :class="{ active: page == 'profile' }" onmousedown={ setPage.bind(null, 'profile') }>%fa:user .fw%%i18n:desktop.tags.mk-settings.profile%</p>
-		<p :class="{ active: page == 'web' }" onmousedown={ setPage.bind(null, 'web') }>%fa:desktop .fw%Web</p>
-		<p :class="{ active: page == 'notification' }" onmousedown={ setPage.bind(null, 'notification') }>%fa:R bell .fw%通知</p>
-		<p :class="{ active: page == 'drive' }" onmousedown={ setPage.bind(null, 'drive') }>%fa:cloud .fw%%i18n:desktop.tags.mk-settings.drive%</p>
-		<p :class="{ active: page == 'mute' }" onmousedown={ setPage.bind(null, 'mute') }>%fa:ban .fw%%i18n:desktop.tags.mk-settings.mute%</p>
-		<p :class="{ active: page == 'apps' }" onmousedown={ setPage.bind(null, 'apps') }>%fa:puzzle-piece .fw%アプリ</p>
-		<p :class="{ active: page == 'twitter' }" onmousedown={ setPage.bind(null, 'twitter') }>%fa:B twitter .fw%Twitter</p>
-		<p :class="{ active: page == 'security' }" onmousedown={ setPage.bind(null, 'security') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p>
-		<p :class="{ active: page == 'api' }" onmousedown={ setPage.bind(null, 'api') }>%fa:key .fw%API</p>
-		<p :class="{ active: page == 'other' }" onmousedown={ setPage.bind(null, 'other') }>%fa:cogs .fw%%i18n:desktop.tags.mk-settings.other%</p>
-	</div>
-	<div class="pages">
-		<section class="profile" show={ page == 'profile' }>
-			<h1>%i18n:desktop.tags.mk-settings.profile%</h1>
-			<mk-profile-setting/>
-		</section>
-
-		<section class="web" show={ page == 'web' }>
-			<h1>デザイン</h1>
-			<a href="/i/customize-home" class="ui button">ホームをカスタマイズ</a>
-		</section>
-
-		<section class="drive" show={ page == 'drive' }>
-			<h1>%i18n:desktop.tags.mk-settings.drive%</h1>
-			<mk-drive-setting/>
-		</section>
-
-		<section class="mute" show={ page == 'mute' }>
-			<h1>%i18n:desktop.tags.mk-settings.mute%</h1>
-			<mk-mute-setting/>
-		</section>
-
-		<section class="apps" show={ page == 'apps' }>
-			<h1>アプリケーション</h1>
-			<mk-authorized-apps/>
-		</section>
-
-		<section class="twitter" show={ page == 'twitter' }>
-			<h1>Twitter</h1>
-			<mk-twitter-setting/>
-		</section>
-
-		<section class="password" show={ page == 'security' }>
-			<h1>%i18n:desktop.tags.mk-settings.password%</h1>
-			<mk-password-setting/>
-		</section>
-
-		<section class="2fa" show={ page == 'security' }>
-			<h1>%i18n:desktop.tags.mk-settings.2fa%</h1>
-			<mk-2fa-setting/>
-		</section>
-
-		<section class="signin" show={ page == 'security' }>
-			<h1>サインイン履歴</h1>
-			<mk-signin-history/>
-		</section>
-
-		<section class="api" show={ page == 'api' }>
-			<h1>API</h1>
-			<mk-api-info/>
-		</section>
-
-		<section class="other" show={ page == 'other' }>
-			<h1>%i18n:desktop.tags.mk-settings.license%</h1>
-			%license%
-		</section>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display flex
-			width 100%
-			height 100%
-
-			> .nav
-				flex 0 0 200px
-				width 100%
-				height 100%
-				padding 16px 0 0 0
-				overflow auto
-				border-right solid 1px #ddd
-
-				> p
-					display block
-					padding 10px 16px
-					margin 0
-					color #666
-					cursor pointer
-					user-select none
-					transition margin-left 0.2s ease
-
-					> [data-fa]
-						margin-right 4px
-
-					&:hover
-						color #555
-
-					&.active
-						margin-left 8px
-						color $theme-color !important
-
-			> .pages
-				width 100%
-				height 100%
-				flex auto
-				overflow auto
-
-				> section
-					margin 32px
-					color #4a535a
-
-					> h1
-						display block
-						margin 0 0 1em 0
-						padding 0 0 8px 0
-						font-size 1em
-						color #555
-						border-bottom solid 1px #eee
-
-	</style>
-	<script lang="typescript">
-		this.page = 'profile';
-
-		this.setPage = page => {
-			this.page = page;
-		};
-	</script>
-</mk-settings>
 
 <mk-profile-setting>
 	<label class="avatar ui from group">
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
new file mode 100644
index 000000000..fe996689a
--- /dev/null
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -0,0 +1,136 @@
+<template>
+<div class="mk-settings">
+	<div class="nav">
+		<p :class="{ active: page == 'profile' }" @mousedown="page = 'profile'">%fa:user .fw%%i18n:desktop.tags.mk-settings.profile%</p>
+		<p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p>
+		<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%通知</p>
+		<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:desktop.tags.mk-settings.drive%</p>
+		<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:desktop.tags.mk-settings.mute%</p>
+		<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%アプリ</p>
+		<p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p>
+		<p :class="{ active: page == 'security' }" @mousedown="page = 'security'">%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p>
+		<p :class="{ active: page == 'api' }" @mousedown="page = 'api'">%fa:key .fw%API</p>
+		<p :class="{ active: page == 'other' }" @mousedown="page = 'other'">%fa:cogs .fw%%i18n:desktop.tags.mk-settings.other%</p>
+	</div>
+	<div class="pages">
+		<section class="profile" v-show="page == 'profile'">
+			<h1>%i18n:desktop.tags.mk-settings.profile%</h1>
+			<mk-profile-setting/>
+		</section>
+
+		<section class="web" v-show="page == 'web'">
+			<h1>デザイン</h1>
+			<a href="/i/customize-home" class="ui button">ホームをカスタマイズ</a>
+		</section>
+
+		<section class="drive" v-show="page == 'drive'">
+			<h1>%i18n:desktop.tags.mk-settings.drive%</h1>
+			<mk-drive-setting/>
+		</section>
+
+		<section class="mute" v-show="page == 'mute'">
+			<h1>%i18n:desktop.tags.mk-settings.mute%</h1>
+			<mk-mute-setting/>
+		</section>
+
+		<section class="apps" v-show="page == 'apps'">
+			<h1>アプリケーション</h1>
+			<mk-authorized-apps/>
+		</section>
+
+		<section class="twitter" v-show="page == 'twitter'">
+			<h1>Twitter</h1>
+			<mk-twitter-setting/>
+		</section>
+
+		<section class="password" v-show="page == 'security'">
+			<h1>%i18n:desktop.tags.mk-settings.password%</h1>
+			<mk-password-setting/>
+		</section>
+
+		<section class="2fa" v-show="page == 'security'">
+			<h1>%i18n:desktop.tags.mk-settings.2fa%</h1>
+			<mk-2fa-setting/>
+		</section>
+
+		<section class="signin" v-show="page == 'security'">
+			<h1>サインイン履歴</h1>
+			<mk-signin-history/>
+		</section>
+
+		<section class="api" v-show="page == 'api'">
+			<h1>API</h1>
+			<mk-api-info/>
+		</section>
+
+		<section class="other" v-show="page == 'other'">
+			<h1>%i18n:desktop.tags.mk-settings.license%</h1>
+			%license%
+		</section>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			page: 'profile'
+		};
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-settings
+	display flex
+	width 100%
+	height 100%
+
+	> .nav
+		flex 0 0 200px
+		width 100%
+		height 100%
+		padding 16px 0 0 0
+		overflow auto
+		border-right solid 1px #ddd
+
+		> p
+			display block
+			padding 10px 16px
+			margin 0
+			color #666
+			cursor pointer
+			user-select none
+			transition margin-left 0.2s ease
+
+			> [data-fa]
+				margin-right 4px
+
+			&:hover
+				color #555
+
+			&.active
+				margin-left 8px
+				color $theme-color !important
+
+	> .pages
+		width 100%
+		height 100%
+		flex auto
+		overflow auto
+
+		> section
+			margin 32px
+			color #4a535a
+
+			> h1
+				display block
+				margin 0 0 1em 0
+				padding 0 0 8px 0
+				font-size 1em
+				color #555
+				border-bottom solid 1px #eee
+
+</style>

From 66b8fcc7e71c4df8dc4ab19cc7b5d19c5d7d2b3a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 23:17:08 +0900
Subject: [PATCH 061/286] wip

---
 src/web/app/desktop/-tags/settings.tag        | 62 ----------------
 .../views/components/profile-setting.vue      | 73 +++++++++++++++++++
 2 files changed, 73 insertions(+), 62 deletions(-)
 create mode 100644 src/web/app/desktop/views/components/profile-setting.vue

diff --git a/src/web/app/desktop/-tags/settings.tag b/src/web/app/desktop/-tags/settings.tag
index f4e2910d8..a9c94181f 100644
--- a/src/web/app/desktop/-tags/settings.tag
+++ b/src/web/app/desktop/-tags/settings.tag
@@ -1,66 +1,4 @@
 
-<mk-profile-setting>
-	<label class="avatar ui from group">
-		<p>%i18n:desktop.tags.mk-profile-setting.avatar%</p><img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		<button class="ui" @click="avatar">%i18n:desktop.tags.mk-profile-setting.choice-avatar%</button>
-	</label>
-	<label class="ui from group">
-		<p>%i18n:desktop.tags.mk-profile-setting.name%</p>
-		<input ref="accountName" type="text" value={ I.name } class="ui"/>
-	</label>
-	<label class="ui from group">
-		<p>%i18n:desktop.tags.mk-profile-setting.location%</p>
-		<input ref="accountLocation" type="text" value={ I.profile.location } class="ui"/>
-	</label>
-	<label class="ui from group">
-		<p>%i18n:desktop.tags.mk-profile-setting.description%</p>
-		<textarea ref="accountDescription" class="ui">{ I.description }</textarea>
-	</label>
-	<label class="ui from group">
-		<p>%i18n:desktop.tags.mk-profile-setting.birthday%</p>
-		<input ref="accountBirthday" type="date" value={ I.profile.birthday } class="ui"/>
-	</label>
-	<button class="ui primary" @click="updateAccount">%i18n:desktop.tags.mk-profile-setting.save%</button>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> .avatar
-				> img
-					display inline-block
-					vertical-align top
-					width 64px
-					height 64px
-					border-radius 4px
-
-				> button
-					margin-left 8px
-
-	</style>
-	<script lang="typescript">
-		import updateAvatar from '../scripts/update-avatar';
-		import notify from '../scripts/notify';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.avatar = () => {
-			updateAvatar(this.I);
-		};
-
-		this.updateAccount = () => {
-			this.api('i/update', {
-				name: this.$refs.accountName.value,
-				location: this.$refs.accountLocation.value || null,
-				description: this.$refs.accountDescription.value || null,
-				birthday: this.$refs.accountBirthday.value || null
-			}).then(() => {
-				notify('プロフィールを更新しました');
-			});
-		};
-	</script>
-</mk-profile-setting>
-
 <mk-api-info>
 	<p>Token: <code>{ I.token }</code></p>
 	<p>%i18n:desktop.tags.mk-api-info.intro%</p>
diff --git a/src/web/app/desktop/views/components/profile-setting.vue b/src/web/app/desktop/views/components/profile-setting.vue
new file mode 100644
index 000000000..abf80d316
--- /dev/null
+++ b/src/web/app/desktop/views/components/profile-setting.vue
@@ -0,0 +1,73 @@
+<template>
+<div class="mk-profile-setting">
+	<label class="avatar ui from group">
+		<p>%i18n:desktop.tags.mk-profile-setting.avatar%</p><img class="avatar" :src="`${$root.$data.os.i.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<button class="ui" @click="updateAvatar">%i18n:desktop.tags.mk-profile-setting.choice-avatar%</button>
+	</label>
+	<label class="ui from group">
+		<p>%i18n:desktop.tags.mk-profile-setting.name%</p>
+		<input v-model="name" type="text" class="ui"/>
+	</label>
+	<label class="ui from group">
+		<p>%i18n:desktop.tags.mk-profile-setting.location%</p>
+		<input v-model="location" type="text" class="ui"/>
+	</label>
+	<label class="ui from group">
+		<p>%i18n:desktop.tags.mk-profile-setting.description%</p>
+		<textarea v-model="description" class="ui"></textarea>
+	</label>
+	<label class="ui from group">
+		<p>%i18n:desktop.tags.mk-profile-setting.birthday%</p>
+		<input v-model="birthday" type="date" class="ui"/>
+	</label>
+	<button class="ui primary" @click="save">%i18n:desktop.tags.mk-profile-setting.save%</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import updateAvatar from '../../scripts/update-avatar';
+import notify from '../../scripts/notify';
+
+export default Vue.extend({
+	data() {
+		return {
+			name: this.$root.$data.os.i.name,
+			location: this.$root.$data.os.i.location,
+			description: this.$root.$data.os.i.description,
+			birthday: this.$root.$data.os.i.birthday,
+		};
+	},
+	methods: {
+		updateAvatar() {
+			updateAvatar(this.$root.$data.os.i);
+		},
+		save() {
+			this.$root.$data.os.api('i/update', {
+				name: this.name,
+				location: this.location || null,
+				description: this.description || null,
+				birthday: this.birthday || null
+			}).then(() => {
+				notify('プロフィールを更新しました');
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-profile-setting
+	> .avatar
+		> img
+			display inline-block
+			vertical-align top
+			width 64px
+			height 64px
+			border-radius 4px
+
+		> button
+			margin-left 8px
+
+</style>
+

From 0ea37056f49a7bfaf6b6b5c1d5f143730464cea5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 23:22:43 +0900
Subject: [PATCH 062/286] wip

---
 src/web/app/desktop/-tags/settings.tag        | 34 -----------------
 .../desktop/views/components/api-setting.vue  | 38 +++++++++++++++++++
 2 files changed, 38 insertions(+), 34 deletions(-)
 create mode 100644 src/web/app/desktop/views/components/api-setting.vue

diff --git a/src/web/app/desktop/-tags/settings.tag b/src/web/app/desktop/-tags/settings.tag
index a9c94181f..2b2491b46 100644
--- a/src/web/app/desktop/-tags/settings.tag
+++ b/src/web/app/desktop/-tags/settings.tag
@@ -1,38 +1,4 @@
 
-<mk-api-info>
-	<p>Token: <code>{ I.token }</code></p>
-	<p>%i18n:desktop.tags.mk-api-info.intro%</p>
-	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-api-info.caution%</p></div>
-	<p>%i18n:desktop.tags.mk-api-info.regeneration-of-token%</p>
-	<button class="ui" @click="regenerateToken">%i18n:desktop.tags.mk-api-info.regenerate-token%</button>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			color #4a535a
-
-			code
-				display inline-block
-				padding 4px 6px
-				color #555
-				background #eee
-				border-radius 2px
-	</style>
-	<script lang="typescript">
-		import passwordDialog from '../scripts/password-dialog';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.regenerateToken = () => {
-			passwordDialog('%i18n:desktop.tags.mk-api-info.enter-password%', password => {
-				this.api('i/regenerate_token', {
-					password: password
-				});
-			});
-		};
-	</script>
-</mk-api-info>
-
 <mk-password-setting>
 	<button @click="reset" class="ui primary">%i18n:desktop.tags.mk-password-setting.reset%</button>
 	<style lang="stylus" scoped>
diff --git a/src/web/app/desktop/views/components/api-setting.vue b/src/web/app/desktop/views/components/api-setting.vue
new file mode 100644
index 000000000..78429064b
--- /dev/null
+++ b/src/web/app/desktop/views/components/api-setting.vue
@@ -0,0 +1,38 @@
+<template>
+<div class="mk-api-setting">
+	<p>Token: <code>{{ $root.$data.os.i.token }}</code></p>
+	<p>%i18n:desktop.tags.mk-api-info.intro%</p>
+	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-api-info.caution%</p></div>
+	<p>%i18n:desktop.tags.mk-api-info.regeneration-of-token%</p>
+	<button class="ui" @click="regenerateToken">%i18n:desktop.tags.mk-api-info.regenerate-token%</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import passwordDialog from '../../scripts/password-dialog';
+
+export default Vue.extend({
+	methods: {
+		regenerateToken() {
+			passwordDialog('%i18n:desktop.tags.mk-api-info.enter-password%', password => {
+				this.$root.$data.os.api('i/regenerate_token', {
+					password: password
+				});
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-api-setting
+	color #4a535a
+
+	code
+		display inline-block
+		padding 4px 6px
+		color #555
+		background #eee
+		border-radius 2px
+</style>

From 7b60dae1b63ac61798631be951103a6d62d3da23 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 23:35:37 +0900
Subject: [PATCH 063/286] wip

---
 src/web/app/desktop/-tags/settings.tag        | 38 -------------------
 .../views/components/password-setting.vue     | 37 ++++++++++++++++++
 2 files changed, 37 insertions(+), 38 deletions(-)
 create mode 100644 src/web/app/desktop/views/components/password-setting.vue

diff --git a/src/web/app/desktop/-tags/settings.tag b/src/web/app/desktop/-tags/settings.tag
index 2b2491b46..2196be87a 100644
--- a/src/web/app/desktop/-tags/settings.tag
+++ b/src/web/app/desktop/-tags/settings.tag
@@ -1,42 +1,4 @@
 
-<mk-password-setting>
-	<button @click="reset" class="ui primary">%i18n:desktop.tags.mk-password-setting.reset%</button>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			color #4a535a
-	</style>
-	<script lang="typescript">
-		import passwordDialog from '../scripts/password-dialog';
-		import dialog from '../scripts/dialog';
-		import notify from '../scripts/notify';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.reset = () => {
-			passwordDialog('%i18n:desktop.tags.mk-password-setting.enter-current-password%', currentPassword => {
-				passwordDialog('%i18n:desktop.tags.mk-password-setting.enter-new-password%', newPassword => {
-					passwordDialog('%i18n:desktop.tags.mk-password-setting.enter-new-password-again%', newPassword2 => {
-						if (newPassword !== newPassword2) {
-							dialog(null, '%i18n:desktop.tags.mk-password-setting.not-match%', [{
-								text: 'OK'
-							}]);
-							return;
-						}
-						this.api('i/change_password', {
-							current_password: currentPassword,
-							new_password: newPassword
-						}).then(() => {
-							notify('%i18n:desktop.tags.mk-password-setting.changed%');
-						});
-					});
-				});
-			});
-		};
-	</script>
-</mk-password-setting>
-
 <mk-2fa-setting>
 	<p>%i18n:desktop.tags.mk-2fa-setting.intro%<a href="%i18n:desktop.tags.mk-2fa-setting.url%" target="_blank">%i18n:desktop.tags.mk-2fa-setting.detail%</a></p>
 	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-2fa-setting.caution%</p></div>
diff --git a/src/web/app/desktop/views/components/password-setting.vue b/src/web/app/desktop/views/components/password-setting.vue
new file mode 100644
index 000000000..2e3e4fb6f
--- /dev/null
+++ b/src/web/app/desktop/views/components/password-setting.vue
@@ -0,0 +1,37 @@
+<template>
+<div>
+	<button @click="reset" class="ui primary">%i18n:desktop.tags.mk-password-setting.reset%</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import passwordDialog from '../../scripts/password-dialog';
+import dialog from '../../scripts/dialog';
+import notify from '../../scripts/notify';
+
+export default Vue.extend({
+	methods: {
+		reset() {
+			passwordDialog('%i18n:desktop.tags.mk-password-setting.enter-current-password%', currentPassword => {
+				passwordDialog('%i18n:desktop.tags.mk-password-setting.enter-new-password%', newPassword => {
+					passwordDialog('%i18n:desktop.tags.mk-password-setting.enter-new-password-again%', newPassword2 => {
+						if (newPassword !== newPassword2) {
+							dialog(null, '%i18n:desktop.tags.mk-password-setting.not-match%', [{
+								text: 'OK'
+							}]);
+							return;
+						}
+						this.$root.$data.os.api('i/change_password', {
+							current_password: currentPassword,
+							new_password: newPassword
+						}).then(() => {
+							notify('%i18n:desktop.tags.mk-password-setting.changed%');
+						});
+					});
+				});
+			});
+		}
+	}
+});
+</script>

From b47c847211c3e8c0ba7d50f2892475d1a47bfa84 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Mon, 12 Feb 2018 23:48:01 +0900
Subject: [PATCH 064/286] wip

---
 src/web/app/desktop/-tags/settings.tag        | 163 ------------------
 .../desktop/views/components/2fa-setting.vue  |  76 ++++++++
 .../desktop/views/components/mute-setting.vue |  31 ++++
 3 files changed, 107 insertions(+), 163 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/settings.tag
 create mode 100644 src/web/app/desktop/views/components/2fa-setting.vue
 create mode 100644 src/web/app/desktop/views/components/mute-setting.vue

diff --git a/src/web/app/desktop/-tags/settings.tag b/src/web/app/desktop/-tags/settings.tag
deleted file mode 100644
index 2196be87a..000000000
--- a/src/web/app/desktop/-tags/settings.tag
+++ /dev/null
@@ -1,163 +0,0 @@
-
-<mk-2fa-setting>
-	<p>%i18n:desktop.tags.mk-2fa-setting.intro%<a href="%i18n:desktop.tags.mk-2fa-setting.url%" target="_blank">%i18n:desktop.tags.mk-2fa-setting.detail%</a></p>
-	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-2fa-setting.caution%</p></div>
-	<p v-if="!data && !I.two_factor_enabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
-	<template v-if="I.two_factor_enabled">
-		<p>%i18n:desktop.tags.mk-2fa-setting.already-registered%</p>
-		<button @click="unregister" class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button>
-	</template>
-	<div v-if="data">
-		<ol>
-			<li>%i18n:desktop.tags.mk-2fa-setting.authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:desktop.tags.mk-2fa-setting.howtoinstall%</a></li>
-			<li>%i18n:desktop.tags.mk-2fa-setting.scan%<br><img src={ data.qr }></li>
-			<li>%i18n:desktop.tags.mk-2fa-setting.done%<br>
-				<input type="number" ref="token" class="ui">
-				<button @click="submit" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.submit%</button>
-			</li>
-		</ol>
-		<div class="ui info"><p>%fa:info-circle%%i18n:desktop.tags.mk-2fa-setting.info%</p></div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			color #4a535a
-
-	</style>
-	<script lang="typescript">
-		import passwordDialog from '../scripts/password-dialog';
-		import notify from '../scripts/notify';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.register = () => {
-			passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => {
-				this.api('i/2fa/register', {
-					password: password
-				}).then(data => {
-					this.update({
-						data: data
-					});
-				});
-			});
-		};
-
-		this.unregister = () => {
-			passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => {
-				this.api('i/2fa/unregister', {
-					password: password
-				}).then(data => {
-					notify('%i18n:desktop.tags.mk-2fa-setting.unregistered%');
-					this.I.two_factor_enabled = false;
-					this.I.update();
-				});
-			});
-		};
-
-		this.submit = () => {
-			this.api('i/2fa/done', {
-				token: this.$refs.token.value
-			}).then(() => {
-				notify('%i18n:desktop.tags.mk-2fa-setting.success%');
-				this.I.two_factor_enabled = true;
-				this.I.update();
-			}).catch(() => {
-				notify('%i18n:desktop.tags.mk-2fa-setting.failed%');
-			});
-		};
-	</script>
-</mk-2fa-setting>
-
-<mk-drive-setting>
-	<svg viewBox="0 0 1 1" preserveAspectRatio="none">
-		<circle
-			riot-r={ r }
-			cx="50%" cy="50%"
-			fill="none"
-			stroke-width="0.1"
-			stroke="rgba(0, 0, 0, 0.05)"/>
-		<circle
-			riot-r={ r }
-			cx="50%" cy="50%"
-			riot-stroke-dasharray={ Math.PI * (r * 2) }
-			riot-stroke-dashoffset={ strokeDashoffset }
-			fill="none"
-			stroke-width="0.1"
-			riot-stroke={ color }/>
-		<text x="50%" y="50%" dy="0.05" text-anchor="middle">{ (usageP * 100).toFixed(0) }%</text>
-	</svg>
-
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			color #4a535a
-
-			> svg
-				display block
-				height 128px
-
-				> circle
-					transform-origin center
-					transform rotate(-90deg)
-					transition stroke-dashoffset 0.5s ease
-
-				> text
-					font-size 0.15px
-					fill rgba(0, 0, 0, 0.6)
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.r = 0.4;
-
-		this.on('mount', () => {
-			this.api('drive').then(info => {
-				const usageP = info.usage / info.capacity;
-				const color = `hsl(${180 - (usageP * 180)}, 80%, 70%)`;
-				const strokeDashoffset = (1 - usageP) * (Math.PI * (this.r * 2));
-
-				this.update({
-					color,
-					strokeDashoffset,
-					usageP,
-					usage: info.usage,
-					capacity: info.capacity
-				});
-			});
-		});
-	</script>
-</mk-drive-setting>
-
-<mk-mute-setting>
-	<div class="none ui info" v-if="!fetching && users.length == 0">
-		<p>%fa:info-circle%%i18n:desktop.tags.mk-mute-setting.no-users%</p>
-	</div>
-	<div class="users" v-if="users.length != 0">
-		<div each={ user in users }>
-			<p><b>{ user.name }</b> @{ user.username }</p>
-		</div>
-	</div>
-
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.apps = [];
-		this.fetching = true;
-
-		this.on('mount', () => {
-			this.api('mute/list').then(x => {
-				this.update({
-					fetching: false,
-					users: x.users
-				});
-			});
-		});
-	</script>
-</mk-mute-setting>
diff --git a/src/web/app/desktop/views/components/2fa-setting.vue b/src/web/app/desktop/views/components/2fa-setting.vue
new file mode 100644
index 000000000..146d707e1
--- /dev/null
+++ b/src/web/app/desktop/views/components/2fa-setting.vue
@@ -0,0 +1,76 @@
+<template>
+<div class="mk-2fa-setting">
+	<p>%i18n:desktop.tags.mk-2fa-setting.intro%<a href="%i18n:desktop.tags.mk-2fa-setting.url%" target="_blank">%i18n:desktop.tags.mk-2fa-setting.detail%</a></p>
+	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-2fa-setting.caution%</p></div>
+	<p v-if="!data && !I.two_factor_enabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
+	<template v-if="I.two_factor_enabled">
+		<p>%i18n:desktop.tags.mk-2fa-setting.already-registered%</p>
+		<button @click="unregister" class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button>
+	</template>
+	<div v-if="data">
+		<ol>
+			<li>%i18n:desktop.tags.mk-2fa-setting.authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:desktop.tags.mk-2fa-setting.howtoinstall%</a></li>
+			<li>%i18n:desktop.tags.mk-2fa-setting.scan%<br><img src={ data.qr }></li>
+			<li>%i18n:desktop.tags.mk-2fa-setting.done%<br>
+				<input type="number" v-model="token" class="ui">
+				<button @click="submit" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.submit%</button>
+			</li>
+		</ol>
+		<div class="ui info"><p>%fa:info-circle%%i18n:desktop.tags.mk-2fa-setting.info%</p></div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import passwordDialog from '../../scripts/password-dialog';
+import notify from '../../scripts/notify';
+
+export default Vue.extend({
+	data() {
+		return {
+			data: null,
+			token: null
+		};
+	},
+	methods: {
+		register() {
+			passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => {
+				this.$root.$data.os.api('i/2fa/register', {
+					password: password
+				}).then(data => {
+					this.data = data;
+				});
+			});
+		},
+
+		unregister() {
+			passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => {
+				this.$root.$data.os.api('i/2fa/unregister', {
+					password: password
+				}).then(() => {
+					notify('%i18n:desktop.tags.mk-2fa-setting.unregistered%');
+					this.$root.$data.os.i.two_factor_enabled = false;
+				});
+			});
+		},
+
+		submit() {
+			this.$root.$data.os.api('i/2fa/done', {
+				token: this.token
+			}).then(() => {
+				notify('%i18n:desktop.tags.mk-2fa-setting.success%');
+				this.$root.$data.os.i.two_factor_enabled = true;
+			}).catch(() => {
+				notify('%i18n:desktop.tags.mk-2fa-setting.failed%');
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-2fa-setting
+	color #4a535a
+
+</style>
diff --git a/src/web/app/desktop/views/components/mute-setting.vue b/src/web/app/desktop/views/components/mute-setting.vue
new file mode 100644
index 000000000..a8813172a
--- /dev/null
+++ b/src/web/app/desktop/views/components/mute-setting.vue
@@ -0,0 +1,31 @@
+<template>
+<div class="mk-mute-setting">
+	<div class="none ui info" v-if="!fetching && users.length == 0">
+		<p>%fa:info-circle%%i18n:desktop.tags.mk-mute-setting.no-users%</p>
+	</div>
+	<div class="users" v-if="users.length != 0">
+		<div v-for="user in users" :key="user.id">
+			<p><b>{{ user.name }}</b> @{{ user.username }}</p>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	data() {
+		return {
+			fetching: true,
+			users: null
+		};
+	},
+	mounted() {
+		this.$root.$data.os.api('mute/list').then(x => {
+			this.fetching = false;
+			this.users = x.users;
+		});
+	}
+});
+</script>

From 44a0952c0f1dd266d45df9333a6e40d641f4b767 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 08:11:10 +0900
Subject: [PATCH 065/286] wip

---
 src/web/app/common/-tags/post-menu.tag        | 157 ----------
 .../app/common/views/components/post-menu.vue | 138 +++++++++
 .../views/components/reaction-picker.vue      | 276 +++++++++---------
 src/web/app/desktop/-tags/index.ts            |  89 ------
 .../desktop/-tags/set-avatar-suggestion.tag   |  48 ---
 .../desktop/-tags/set-banner-suggestion.tag   |  48 ---
 .../views/components/sub-post-content.vue     |  33 ++-
 .../views/components/timeline-post.vue        | 137 +++++----
 8 files changed, 357 insertions(+), 569 deletions(-)
 delete mode 100644 src/web/app/common/-tags/post-menu.tag
 create mode 100644 src/web/app/common/views/components/post-menu.vue
 delete mode 100644 src/web/app/desktop/-tags/index.ts
 delete mode 100644 src/web/app/desktop/-tags/set-avatar-suggestion.tag
 delete mode 100644 src/web/app/desktop/-tags/set-banner-suggestion.tag

diff --git a/src/web/app/common/-tags/post-menu.tag b/src/web/app/common/-tags/post-menu.tag
deleted file mode 100644
index c2b362e8b..000000000
--- a/src/web/app/common/-tags/post-menu.tag
+++ /dev/null
@@ -1,157 +0,0 @@
-<mk-post-menu>
-	<div class="backdrop" ref="backdrop" @click="close"></div>
-	<div class="popover { compact: opts.compact }" ref="popover">
-		<button v-if="post.user_id === I.id" @click="pin">%i18n:common.tags.mk-post-menu.pin%</button>
-		<div v-if="I.is_pro && !post.is_category_verified">
-			<select ref="categorySelect">
-				<option value="">%i18n:common.tags.mk-post-menu.select%</option>
-				<option value="music">%i18n:common.post_categories.music%</option>
-				<option value="game">%i18n:common.post_categories.game%</option>
-				<option value="anime">%i18n:common.post_categories.anime%</option>
-				<option value="it">%i18n:common.post_categories.it%</option>
-				<option value="gadgets">%i18n:common.post_categories.gadgets%</option>
-				<option value="photography">%i18n:common.post_categories.photography%</option>
-			</select>
-			<button @click="categorize">%i18n:common.tags.mk-post-menu.categorize%</button>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		$border-color = rgba(27, 31, 35, 0.15)
-
-		:scope
-			display block
-			position initial
-
-			> .backdrop
-				position fixed
-				top 0
-				left 0
-				z-index 10000
-				width 100%
-				height 100%
-				background rgba(0, 0, 0, 0.1)
-				opacity 0
-
-			> .popover
-				position absolute
-				z-index 10001
-				background #fff
-				border 1px solid $border-color
-				border-radius 4px
-				box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
-				transform scale(0.5)
-				opacity 0
-
-				$balloon-size = 16px
-
-				&:not(.compact)
-					margin-top $balloon-size
-					transform-origin center -($balloon-size)
-
-					&:before
-						content ""
-						display block
-						position absolute
-						top -($balloon-size * 2)
-						left s('calc(50% - %s)', $balloon-size)
-						border-top solid $balloon-size transparent
-						border-left solid $balloon-size transparent
-						border-right solid $balloon-size transparent
-						border-bottom solid $balloon-size $border-color
-
-					&:after
-						content ""
-						display block
-						position absolute
-						top -($balloon-size * 2) + 1.5px
-						left s('calc(50% - %s)', $balloon-size)
-						border-top solid $balloon-size transparent
-						border-left solid $balloon-size transparent
-						border-right solid $balloon-size transparent
-						border-bottom solid $balloon-size #fff
-
-				> button
-					display block
-
-	</style>
-	<script lang="typescript">
-		import anime from 'animejs';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.post = this.opts.post;
-		this.source = this.opts.source;
-
-		this.on('mount', () => {
-			const rect = this.source.getBoundingClientRect();
-			const width = this.$refs.popover.offsetWidth;
-			const height = this.$refs.popover.offsetHeight;
-			if (this.opts.compact) {
-				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-				const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
-				this.$refs.popover.style.left = (x - (width / 2)) + 'px';
-				this.$refs.popover.style.top = (y - (height / 2)) + 'px';
-			} else {
-				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-				const y = rect.top + window.pageYOffset + this.source.offsetHeight;
-				this.$refs.popover.style.left = (x - (width / 2)) + 'px';
-				this.$refs.popover.style.top = y + 'px';
-			}
-
-			anime({
-				targets: this.$refs.backdrop,
-				opacity: 1,
-				duration: 100,
-				easing: 'linear'
-			});
-
-			anime({
-				targets: this.$refs.popover,
-				opacity: 1,
-				scale: [0.5, 1],
-				duration: 500
-			});
-		});
-
-		this.pin = () => {
-			this.api('i/pin', {
-				post_id: this.post.id
-			}).then(() => {
-				if (this.opts.cb) this.opts.cb('pinned', '%i18n:common.tags.mk-post-menu.pinned%');
-				this.$destroy();
-			});
-		};
-
-		this.categorize = () => {
-			const category = this.$refs.categorySelect.options[this.$refs.categorySelect.selectedIndex].value;
-			this.api('posts/categorize', {
-				post_id: this.post.id,
-				category: category
-			}).then(() => {
-				if (this.opts.cb) this.opts.cb('categorized', '%i18n:common.tags.mk-post-menu.categorized%');
-				this.$destroy();
-			});
-		};
-
-		this.close = () => {
-			this.$refs.backdrop.style.pointerEvents = 'none';
-			anime({
-				targets: this.$refs.backdrop,
-				opacity: 0,
-				duration: 200,
-				easing: 'linear'
-			});
-
-			this.$refs.popover.style.pointerEvents = 'none';
-			anime({
-				targets: this.$refs.popover,
-				opacity: 0,
-				scale: 0.5,
-				duration: 200,
-				easing: 'easeInBack',
-				complete: () => this.$destroy()
-			});
-		};
-	</script>
-</mk-post-menu>
diff --git a/src/web/app/common/views/components/post-menu.vue b/src/web/app/common/views/components/post-menu.vue
new file mode 100644
index 000000000..078e4745a
--- /dev/null
+++ b/src/web/app/common/views/components/post-menu.vue
@@ -0,0 +1,138 @@
+<template>
+<div class="mk-post-menu">
+	<div class="backdrop" ref="backdrop" @click="close"></div>
+	<div class="popover { compact: opts.compact }" ref="popover">
+		<button v-if="post.user_id === I.id" @click="pin">%i18n:common.tags.mk-post-menu.pin%</button>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import anime from 'animejs';
+
+export default Vue.extend({
+	props: ['post', 'source', 'compact'],
+	mounted() {
+		const popover = this.$refs.popover as any;
+
+		const rect = this.source.getBoundingClientRect();
+		const width = popover.offsetWidth;
+		const height = popover.offsetHeight;
+
+		if (this.compact) {
+			const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+			const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
+			popover.style.left = (x - (width / 2)) + 'px';
+			popover.style.top = (y - (height / 2)) + 'px';
+		} else {
+			const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+			const y = rect.top + window.pageYOffset + this.source.offsetHeight;
+			popover.style.left = (x - (width / 2)) + 'px';
+			popover.style.top = y + 'px';
+		}
+
+		anime({
+			targets: this.$refs.backdrop,
+			opacity: 1,
+			duration: 100,
+			easing: 'linear'
+		});
+
+		anime({
+			targets: this.$refs.popover,
+			opacity: 1,
+			scale: [0.5, 1],
+			duration: 500
+		});
+	},
+	methods: {
+		pin() {
+			this.$root.$data.os.api('i/pin', {
+				post_id: this.post.id
+			}).then(() => {
+				this.$destroy();
+			});
+		},
+
+		close() {
+			(this.$refs.backdrop as any).style.pointerEvents = 'none';
+			anime({
+				targets: this.$refs.backdrop,
+				opacity: 0,
+				duration: 200,
+				easing: 'linear'
+			});
+
+			(this.$refs.popover as any).style.pointerEvents = 'none';
+			anime({
+				targets: this.$refs.popover,
+				opacity: 0,
+				scale: 0.5,
+				duration: 200,
+				easing: 'easeInBack',
+				complete: () => this.$destroy()
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+$border-color = rgba(27, 31, 35, 0.15)
+
+.mk-post-menu
+	position initial
+
+	> .backdrop
+		position fixed
+		top 0
+		left 0
+		z-index 10000
+		width 100%
+		height 100%
+		background rgba(0, 0, 0, 0.1)
+		opacity 0
+
+	> .popover
+		position absolute
+		z-index 10001
+		background #fff
+		border 1px solid $border-color
+		border-radius 4px
+		box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
+		transform scale(0.5)
+		opacity 0
+
+		$balloon-size = 16px
+
+		&:not(.compact)
+			margin-top $balloon-size
+			transform-origin center -($balloon-size)
+
+			&:before
+				content ""
+				display block
+				position absolute
+				top -($balloon-size * 2)
+				left s('calc(50% - %s)', $balloon-size)
+				border-top solid $balloon-size transparent
+				border-left solid $balloon-size transparent
+				border-right solid $balloon-size transparent
+				border-bottom solid $balloon-size $border-color
+
+			&:after
+				content ""
+				display block
+				position absolute
+				top -($balloon-size * 2) + 1.5px
+				left s('calc(50% - %s)', $balloon-size)
+				border-top solid $balloon-size transparent
+				border-left solid $balloon-size transparent
+				border-right solid $balloon-size transparent
+				border-bottom solid $balloon-size #fff
+
+		> button
+			display block
+
+</style>
diff --git a/src/web/app/common/views/components/reaction-picker.vue b/src/web/app/common/views/components/reaction-picker.vue
index dd4d1380b..62ccbfdd0 100644
--- a/src/web/app/common/views/components/reaction-picker.vue
+++ b/src/web/app/common/views/components/reaction-picker.vue
@@ -1,5 +1,5 @@
 <template>
-<div>
+<div class="mk-reaction-picker">
 	<div class="backdrop" ref="backdrop" @click="close"></div>
 	<div class="popover" :class="{ compact }" ref="popover">
 		<p v-if="!compact">{{ title }}</p>
@@ -18,171 +18,169 @@
 </div>
 </template>
 
-<script lang="typescript">
-	import anime from 'animejs';
-	import api from '../scripts/api';
-	import MkReactionIcon from './reaction-icon.vue';
+<script lang="ts">
+import Vue from 'vue';
+import anime from 'animejs';
 
-	const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%';
+const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%';
 
-	export default {
-		components: {
-			MkReactionIcon
+export default Vue.extend({
+	props: ['post', 'source', 'compact', 'cb'],
+	data() {
+		return {
+			title: placeholder
+		};
+	},
+	mounted() {
+		const popover = this.$refs.popover as any;
+
+		const rect = this.source.getBoundingClientRect();
+		const width = popover.offsetWidth;
+		const height = popover.offsetHeight;
+
+		if (this.compact) {
+			const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+			const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
+			popover.style.left = (x - (width / 2)) + 'px';
+			popover.style.top = (y - (height / 2)) + 'px';
+		} else {
+			const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+			const y = rect.top + window.pageYOffset + this.source.offsetHeight;
+			popover.style.left = (x - (width / 2)) + 'px';
+			popover.style.top = y + 'px';
+		}
+
+		anime({
+			targets: this.$refs.backdrop,
+			opacity: 1,
+			duration: 100,
+			easing: 'linear'
+		});
+
+		anime({
+			targets: this.$refs.popover,
+			opacity: 1,
+			scale: [0.5, 1],
+			duration: 500
+		});
+	},
+	methods: {
+		react(reaction) {
+			this.$root.$data.os.api('posts/reactions/create', {
+				post_id: this.post.id,
+				reaction: reaction
+			}).then(() => {
+				if (this.cb) this.cb();
+				this.$destroy();
+			});
 		},
-		props: ['post', 'source', 'compact', 'cb'],
-		data() {
-			return {
-				title: placeholder
-			};
+		onMouseover(e) {
+			this.title = e.target.title;
 		},
-		created() {
-			const rect = this.source.getBoundingClientRect();
-			const width = this.$refs.popover.offsetWidth;
-			const height = this.$refs.popover.offsetHeight;
-			if (this.compact) {
-				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-				const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
-				this.$refs.popover.style.left = (x - (width / 2)) + 'px';
-				this.$refs.popover.style.top = (y - (height / 2)) + 'px';
-			} else {
-				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-				const y = rect.top + window.pageYOffset + this.source.offsetHeight;
-				this.$refs.popover.style.left = (x - (width / 2)) + 'px';
-				this.$refs.popover.style.top = y + 'px';
-			}
-
+		onMouseout(e) {
+			this.title = placeholder;
+		},
+		close() {
+			(this.$refs.backdrop as any).style.pointerEvents = 'none';
 			anime({
 				targets: this.$refs.backdrop,
-				opacity: 1,
-				duration: 100,
+				opacity: 0,
+				duration: 200,
 				easing: 'linear'
 			});
 
+			(this.$refs.popover as any).style.pointerEvents = 'none';
 			anime({
 				targets: this.$refs.popover,
-				opacity: 1,
-				scale: [0.5, 1],
-				duration: 500
+				opacity: 0,
+				scale: 0.5,
+				duration: 200,
+				easing: 'easeInBack',
+				complete: () => this.$destroy()
 			});
-		},
-		methods: {
-			react(reaction) {
-				api('posts/reactions/create', {
-					post_id: this.post.id,
-					reaction: reaction
-				}).then(() => {
-					if (this.cb) this.cb();
-					this.$destroy();
-				});
-			},
-			onMouseover(e) {
-				this.title = e.target.title;
-			},
-			onMouseout(e) {
-				this.title = placeholder;
-			},
-			close() {
-				this.$refs.backdrop.style.pointerEvents = 'none';
-				anime({
-					targets: this.$refs.backdrop,
-					opacity: 0,
-					duration: 200,
-					easing: 'linear'
-				});
-
-				this.$refs.popover.style.pointerEvents = 'none';
-				anime({
-					targets: this.$refs.popover,
-					opacity: 0,
-					scale: 0.5,
-					duration: 200,
-					easing: 'easeInBack',
-					complete: () => this.$destroy()
-				});
-			}
 		}
-	};
+	}
+});
 </script>
 
 <style lang="stylus" scoped>
 	$border-color = rgba(27, 31, 35, 0.15)
 
-	:scope
-		display block
-		position initial
+.mk-reaction-picker
+	position initial
 
-		> .backdrop
-			position fixed
-			top 0
-			left 0
-			z-index 10000
-			width 100%
-			height 100%
-			background rgba(0, 0, 0, 0.1)
-			opacity 0
+	> .backdrop
+		position fixed
+		top 0
+		left 0
+		z-index 10000
+		width 100%
+		height 100%
+		background rgba(0, 0, 0, 0.1)
+		opacity 0
 
-		> .popover
-			position absolute
-			z-index 10001
-			background #fff
-			border 1px solid $border-color
-			border-radius 4px
-			box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
-			transform scale(0.5)
-			opacity 0
+	> .popover
+		position absolute
+		z-index 10001
+		background #fff
+		border 1px solid $border-color
+		border-radius 4px
+		box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
+		transform scale(0.5)
+		opacity 0
 
-			$balloon-size = 16px
+		$balloon-size = 16px
 
-			&:not(.compact)
-				margin-top $balloon-size
-				transform-origin center -($balloon-size)
+		&:not(.compact)
+			margin-top $balloon-size
+			transform-origin center -($balloon-size)
 
-				&:before
-					content ""
-					display block
-					position absolute
-					top -($balloon-size * 2)
-					left s('calc(50% - %s)', $balloon-size)
-					border-top solid $balloon-size transparent
-					border-left solid $balloon-size transparent
-					border-right solid $balloon-size transparent
-					border-bottom solid $balloon-size $border-color
-
-				&:after
-					content ""
-					display block
-					position absolute
-					top -($balloon-size * 2) + 1.5px
-					left s('calc(50% - %s)', $balloon-size)
-					border-top solid $balloon-size transparent
-					border-left solid $balloon-size transparent
-					border-right solid $balloon-size transparent
-					border-bottom solid $balloon-size #fff
-
-			> p
+			&:before
+				content ""
 				display block
-				margin 0
-				padding 8px 10px
-				font-size 14px
-				color #586069
-				border-bottom solid 1px #e1e4e8
+				position absolute
+				top -($balloon-size * 2)
+				left s('calc(50% - %s)', $balloon-size)
+				border-top solid $balloon-size transparent
+				border-left solid $balloon-size transparent
+				border-right solid $balloon-size transparent
+				border-bottom solid $balloon-size $border-color
 
-			> div
-				padding 4px
-				width 240px
-				text-align center
+			&:after
+				content ""
+				display block
+				position absolute
+				top -($balloon-size * 2) + 1.5px
+				left s('calc(50% - %s)', $balloon-size)
+				border-top solid $balloon-size transparent
+				border-left solid $balloon-size transparent
+				border-right solid $balloon-size transparent
+				border-bottom solid $balloon-size #fff
 
-				> button
-					width 40px
-					height 40px
-					font-size 24px
-					border-radius 2px
+		> p
+			display block
+			margin 0
+			padding 8px 10px
+			font-size 14px
+			color #586069
+			border-bottom solid 1px #e1e4e8
 
-					&:hover
-						background #eee
+		> div
+			padding 4px
+			width 240px
+			text-align center
 
-					&:active
-						background $theme-color
-						box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
+			> button
+				width 40px
+				height 40px
+				font-size 24px
+				border-radius 2px
+
+				&:hover
+					background #eee
+
+				&:active
+					background $theme-color
+					box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
 
 </style>
diff --git a/src/web/app/desktop/-tags/index.ts b/src/web/app/desktop/-tags/index.ts
deleted file mode 100644
index 4edda8353..000000000
--- a/src/web/app/desktop/-tags/index.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-require('./contextmenu.tag');
-require('./dialog.tag');
-require('./window.tag');
-require('./input-dialog.tag');
-require('./follow-button.tag');
-require('./drive/base-contextmenu.tag');
-require('./drive/file-contextmenu.tag');
-require('./drive/folder-contextmenu.tag');
-require('./drive/file.tag');
-require('./drive/folder.tag');
-require('./drive/nav-folder.tag');
-require('./drive/browser-window.tag');
-require('./drive/browser.tag');
-require('./select-file-from-drive-window.tag');
-require('./select-folder-from-drive-window.tag');
-require('./crop-window.tag');
-require('./settings.tag');
-require('./settings-window.tag');
-require('./analog-clock.tag');
-require('./notifications.tag');
-require('./post-form-window.tag');
-require('./post-form.tag');
-require('./post-preview.tag');
-require('./repost-form-window.tag');
-require('./home-widgets/user-recommendation.tag');
-require('./home-widgets/timeline.tag');
-require('./home-widgets/mentions.tag');
-require('./home-widgets/calendar.tag');
-require('./home-widgets/donation.tag');
-require('./home-widgets/tips.tag');
-require('./home-widgets/nav.tag');
-require('./home-widgets/profile.tag');
-require('./home-widgets/notifications.tag');
-require('./home-widgets/rss-reader.tag');
-require('./home-widgets/photo-stream.tag');
-require('./home-widgets/broadcast.tag');
-require('./home-widgets/version.tag');
-require('./home-widgets/recommended-polls.tag');
-require('./home-widgets/trends.tag');
-require('./home-widgets/activity.tag');
-require('./home-widgets/server.tag');
-require('./home-widgets/slideshow.tag');
-require('./home-widgets/channel.tag');
-require('./home-widgets/timemachine.tag');
-require('./home-widgets/post-form.tag');
-require('./home-widgets/access-log.tag');
-require('./home-widgets/messaging.tag');
-require('./timeline.tag');
-require('./messaging/window.tag');
-require('./messaging/room-window.tag');
-require('./following-setuper.tag');
-require('./ellipsis-icon.tag');
-require('./ui.tag');
-require('./home.tag');
-require('./user-timeline.tag');
-require('./user.tag');
-require('./big-follow-button.tag');
-require('./pages/entrance.tag');
-require('./pages/home.tag');
-require('./pages/home-customize.tag');
-require('./pages/user.tag');
-require('./pages/post.tag');
-require('./pages/search.tag');
-require('./pages/not-found.tag');
-require('./pages/selectdrive.tag');
-require('./pages/drive.tag');
-require('./pages/messaging-room.tag');
-require('./autocomplete-suggestion.tag');
-require('./progress-dialog.tag');
-require('./user-preview.tag');
-require('./post-detail.tag');
-require('./post-detail-sub.tag');
-require('./search.tag');
-require('./search-posts.tag');
-require('./set-avatar-suggestion.tag');
-require('./set-banner-suggestion.tag');
-require('./repost-form.tag');
-require('./sub-post-content.tag');
-require('./images.tag');
-require('./donation.tag');
-require('./users-list.tag');
-require('./user-following.tag');
-require('./user-followers.tag');
-require('./user-following-window.tag');
-require('./user-followers-window.tag');
-require('./list-user.tag');
-require('./detailed-post-window.tag');
-require('./widgets/calendar.tag');
-require('./widgets/activity.tag');
diff --git a/src/web/app/desktop/-tags/set-avatar-suggestion.tag b/src/web/app/desktop/-tags/set-avatar-suggestion.tag
deleted file mode 100644
index e67a8c66d..000000000
--- a/src/web/app/desktop/-tags/set-avatar-suggestion.tag
+++ /dev/null
@@ -1,48 +0,0 @@
-<mk-set-avatar-suggestion @click="set">
-	<p><b>アバターを設定</b>してみませんか?
-		<button @click="close">%fa:times%</button>
-	</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			cursor pointer
-			color #fff
-			background #a8cad0
-
-			&:hover
-				background #70abb5
-
-			> p
-				display block
-				margin 0 auto
-				padding 8px
-				max-width 1024px
-
-				> a
-					font-weight bold
-					color #fff
-
-				> button
-					position absolute
-					top 0
-					right 0
-					padding 8px
-					color #fff
-
-	</style>
-	<script lang="typescript">
-		import updateAvatar from '../scripts/update-avatar';
-
-		this.mixin('i');
-
-		this.set = () => {
-			updateAvatar(this.I);
-		};
-
-		this.close = e => {
-			e.preventDefault();
-			e.stopPropagation();
-			this.$destroy();
-		};
-	</script>
-</mk-set-avatar-suggestion>
diff --git a/src/web/app/desktop/-tags/set-banner-suggestion.tag b/src/web/app/desktop/-tags/set-banner-suggestion.tag
deleted file mode 100644
index 0d32c9a0e..000000000
--- a/src/web/app/desktop/-tags/set-banner-suggestion.tag
+++ /dev/null
@@ -1,48 +0,0 @@
-<mk-set-banner-suggestion @click="set">
-	<p><b>バナーを設定</b>してみませんか?
-		<button @click="close">%fa:times%</button>
-	</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			cursor pointer
-			color #fff
-			background #a8cad0
-
-			&:hover
-				background #70abb5
-
-			> p
-				display block
-				margin 0 auto
-				padding 8px
-				max-width 1024px
-
-				> a
-					font-weight bold
-					color #fff
-
-				> button
-					position absolute
-					top 0
-					right 0
-					padding 8px
-					color #fff
-
-	</style>
-	<script lang="typescript">
-		import updateBanner from '../scripts/update-banner';
-
-		this.mixin('i');
-
-		this.set = () => {
-			updateBanner(this.I);
-		};
-
-		this.close = e => {
-			e.preventDefault();
-			e.stopPropagation();
-			this.$destroy();
-		};
-	</script>
-</mk-set-banner-suggestion>
diff --git a/src/web/app/desktop/views/components/sub-post-content.vue b/src/web/app/desktop/views/components/sub-post-content.vue
index 2463e8a9b..e5264cefc 100644
--- a/src/web/app/desktop/views/components/sub-post-content.vue
+++ b/src/web/app/desktop/views/components/sub-post-content.vue
@@ -2,8 +2,9 @@
 <div class="mk-sub-post-content">
 	<div class="body">
 		<a class="reply" v-if="post.reply_id">%fa:reply%</a>
-		<span ref="text"></span>
+		<mk-post-html :ast="post.ast" :i="$root.$data.os.i"/>
 		<a class="quote" v-if="post.repost_id" :href="`/post:${post.repost_id}`">RP: ...</a>
+		<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 	</div>
 	<details v-if="post.media">
 		<summary>({{ post.media.length }}つのメディア)</summary>
@@ -16,23 +17,23 @@
 </div>
 </template>
 
-<script lang="typescript">
-	import compile from '../../common/scripts/text-compiler';
+<script lang="ts">
+import Vue from 'vue';
 
-	this.mixin('user-preview');
-
-	this.post = this.opts.post;
-
-	this.on('mount', () => {
-		if (this.post.text) {
-			const tokens = this.post.ast;
-			this.$refs.text.innerHTML = compile(tokens, false);
-
-			Array.from(this.$refs.text.children).forEach(e => {
-				if (e.tagName == 'MK-URL') riot.mount(e);
-			});
+export default Vue.extend({
+	props: ['post'],
+	computed: {
+		urls(): string[] {
+			if (this.post.ast) {
+				return this.post.ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
 		}
-	});
+	}
+});
 </script>
 
 <style lang="stylus" scoped>
diff --git a/src/web/app/desktop/views/components/timeline-post.vue b/src/web/app/desktop/views/components/timeline-post.vue
index c18cff36a..6c3d525d5 100644
--- a/src/web/app/desktop/views/components/timeline-post.vue
+++ b/src/web/app/desktop/views/components/timeline-post.vue
@@ -76,6 +76,19 @@ import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import MkPostFormWindow from './post-form-window.vue';
 import MkRepostFormWindow from './repost-form-window.vue';
+import MkPostMenu from '../../../common/views/components/post-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+
+function focus(el, fn) {
+	const target = fn(el);
+	if (target) {
+		if (target.hasAttribute('tabindex')) {
+			target.focus();
+		} else {
+			focus(target, fn);
+		}
+	}
+}
 
 export default Vue.extend({
 	props: ['post'],
@@ -171,83 +184,63 @@ export default Vue.extend({
 					post: this.p
 				}
 			}).$mount().$el);
+		},
+		react() {
+			document.body.appendChild(new MkReactionPicker({
+				propsData: {
+					source: this.$refs.menuButton,
+					post: this.p
+				}
+			}).$mount().$el);
+		},
+		menu() {
+			document.body.appendChild(new MkPostMenu({
+				propsData: {
+					source: this.$refs.menuButton,
+					post: this.p
+				}
+			}).$mount().$el);
+		},
+		onKeydown(e) {
+			let shouldBeCancel = true;
+
+			switch (true) {
+				case e.which == 38: // [↑]
+				case e.which == 74: // [j]
+				case e.which == 9 && e.shiftKey: // [Shift] + [Tab]
+					focus(this.$el, e => e.previousElementSibling);
+					break;
+
+				case e.which == 40: // [↓]
+				case e.which == 75: // [k]
+				case e.which == 9: // [Tab]
+					focus(this.$el, e => e.nextElementSibling);
+					break;
+
+				case e.which == 81: // [q]
+				case e.which == 69: // [e]
+					this.repost();
+					break;
+
+				case e.which == 70: // [f]
+				case e.which == 76: // [l]
+					//this.like();
+					break;
+
+				case e.which == 82: // [r]
+					this.reply();
+					break;
+
+				default:
+					shouldBeCancel = false;
+			}
+
+			if (shouldBeCancel) e.preventDefault();
 		}
 	}
 });
 </script>
 
-<script lang="typescript">
-
-
-this.react = () => {
-	riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), {
-		source: this.$refs.reactButton,
-		post: this.p
-	});
-};
-
-this.menu = () => {
-	riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
-		source: this.$refs.menuButton,
-		post: this.p
-	});
-};
-
-this.onKeyDown = e => {
-	let shouldBeCancel = true;
-
-	switch (true) {
-		case e.which == 38: // [↑]
-		case e.which == 74: // [j]
-		case e.which == 9 && e.shiftKey: // [Shift] + [Tab]
-			focus(this.root, e => e.previousElementSibling);
-			break;
-
-		case e.which == 40: // [↓]
-		case e.which == 75: // [k]
-		case e.which == 9: // [Tab]
-			focus(this.root, e => e.nextElementSibling);
-			break;
-
-		case e.which == 81: // [q]
-		case e.which == 69: // [e]
-			this.repost();
-			break;
-
-		case e.which == 70: // [f]
-		case e.which == 76: // [l]
-			this.like();
-			break;
-
-		case e.which == 82: // [r]
-			this.reply();
-			break;
-
-		default:
-			shouldBeCancel = false;
-	}
-
-	if (shouldBeCancel) e.preventDefault();
-};
-
-this.onDblClick = () => {
-	riot.mount(document.body.appendChild(document.createElement('mk-detailed-post-window')), {
-		post: this.p.id
-	});
-};
-
-function focus(el, fn) {
-	const target = fn(el);
-	if (target) {
-		if (target.hasAttribute('tabindex')) {
-			target.focus();
-		} else {
-			focus(target, fn);
-		}
-	}
-}
-</script>
-
 <style lang="stylus" scoped>
 .mk-timeline-post
 	margin 0

From 01588085017ea780a1e71f7309b525cb449a65f2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 08:24:44 +0900
Subject: [PATCH 066/286] wip

---
 src/web/app/desktop/-tags/settings-window.tag | 30 -------------------
 src/web/app/desktop/views/components/index.ts | 22 ++++++++++++--
 .../views/components/settings-window.vue      | 15 ++++++++++
 .../app/desktop/views/components/timeline.vue |  7 ++++-
 .../views/components/ui-header-account.vue    |  4 ++-
 .../views/components/ui-header-nav.vue        |  3 +-
 .../views/components/ui-header-search.vue     |  2 +-
 .../views/components/ui-notification.vue      |  2 +-
 8 files changed, 48 insertions(+), 37 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/settings-window.tag
 create mode 100644 src/web/app/desktop/views/components/settings-window.vue

diff --git a/src/web/app/desktop/-tags/settings-window.tag b/src/web/app/desktop/-tags/settings-window.tag
deleted file mode 100644
index 094225f61..000000000
--- a/src/web/app/desktop/-tags/settings-window.tag
+++ /dev/null
@@ -1,30 +0,0 @@
-<mk-settings-window>
-	<mk-window ref="window" is-modal={ true } width={ '700px' } height={ '550px' }>
-		<yield to="header">%fa:cog%設定</yield>
-		<yield to="content">
-			<mk-settings/>
-		</yield>
-	</mk-window>
-	<style lang="stylus" scoped>
-		:scope
-			> mk-window
-				[data-yield='header']
-					> [data-fa]
-						margin-right 4px
-
-				[data-yield='content']
-					overflow hidden
-
-	</style>
-	<script lang="typescript">
-		this.on('mount', () => {
-			this.$refs.window.on('closed', () => {
-				this.$destroy();
-			});
-		});
-
-		this.close = () => {
-			this.$refs.window.close();
-		};
-	</script>
-</mk-settings-window>
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 9788a27f1..71a049a62 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -1,6 +1,14 @@
 import Vue from 'vue';
 
 import ui from './ui.vue';
+import uiHeader from './ui-header.vue';
+import uiHeaderAccount from './ui-header-account.vue';
+import uiHeaderClock from './ui-header-clock.vue';
+import uiHeaderNav from './ui-header-nav.vue';
+import uiHeaderNotifications from './ui-header-notifications.vue';
+import uiHeaderPostButton from './ui-header-post-button.vue';
+import uiHeaderSearch from './ui-header-search.vue';
+import uiNotification from './ui-notification.vue';
 import home from './home.vue';
 import timeline from './timeline.vue';
 import timelinePost from './timeline-post.vue';
@@ -9,13 +17,23 @@ import subPostContent from './sub-post-content.vue';
 import window from './window.vue';
 import postFormWindow from './post-form-window.vue';
 import repostFormWindow from './repost-form-window.vue';
+import analogClock from './analog-clock.vue';
 
 Vue.component('mk-ui', ui);
+Vue.component('mk-ui-header', uiHeader);
+Vue.component('mk-ui-header-account', uiHeaderAccount);
+Vue.component('mk-ui-header-clock', uiHeaderClock);
+Vue.component('mk-ui-header-nav', uiHeaderNav);
+Vue.component('mk-ui-header-notifications', uiHeaderNotifications);
+Vue.component('mk-ui-header-post-button', uiHeaderPostButton);
+Vue.component('mk-ui-header-search', uiHeaderSearch);
+Vue.component('mk-ui-notification', uiNotification);
 Vue.component('mk-home', home);
 Vue.component('mk-timeline', timeline);
 Vue.component('mk-timeline-post', timelinePost);
 Vue.component('mk-timeline-post-sub', timelinePostSub);
 Vue.component('mk-sub-post-content', subPostContent);
 Vue.component('mk-window', window);
-Vue.component('post-form-window', postFormWindow);
-Vue.component('repost-form-window', repostFormWindow);
+Vue.component('mk-post-form-window', postFormWindow);
+Vue.component('mk-repost-form-window', repostFormWindow);
+Vue.component('mk-analog-clock', analogClock);
diff --git a/src/web/app/desktop/views/components/settings-window.vue b/src/web/app/desktop/views/components/settings-window.vue
new file mode 100644
index 000000000..56d839851
--- /dev/null
+++ b/src/web/app/desktop/views/components/settings-window.vue
@@ -0,0 +1,15 @@
+<template>
+<mk-window ref="window" is-modal width='700px' height='550px' @closed="$destroy">
+	<span slot="header" :class="$style.header">%fa:cog%設定</span>
+	<div to="content">
+		<mk-settings/>
+	</div>
+</mk-window>
+</template>
+
+<style lang="stylus" module>
+.header
+	> [data-fa]
+		margin-right 4px
+
+</style>
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index 161eebdf7..933e44825 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -14,7 +14,12 @@
 import Vue from 'vue';
 
 export default Vue.extend({
-	props: ['posts'],
+	props: {
+		posts: {
+			type: Array,
+			default: []
+		}
+	},
 	computed: {
 		_posts(): any {
 			return this.posts.map(post => {
diff --git a/src/web/app/desktop/views/components/ui-header-account.vue b/src/web/app/desktop/views/components/ui-header-account.vue
index 435a0dcaf..8dbd9e5e3 100644
--- a/src/web/app/desktop/views/components/ui-header-account.vue
+++ b/src/web/app/desktop/views/components/ui-header-account.vue
@@ -32,6 +32,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import MkSettingsWindow from './settings-window.vue';
 import contains from '../../../common/scripts/contains';
 import signout from '../../../common/scripts/signout';
 
@@ -68,7 +69,8 @@ export default Vue.extend({
 		},
 		drive() {
 			this.close();
-			document.body.appendChild(new MkDriveWindow().$mount().$el);
+			// TODO
+			//document.body.appendChild(new MkDriveWindow().$mount().$el);
 		},
 		settings() {
 			this.close();
diff --git a/src/web/app/desktop/views/components/ui-header-nav.vue b/src/web/app/desktop/views/components/ui-header-nav.vue
index 5295787b9..d0092ebd2 100644
--- a/src/web/app/desktop/views/components/ui-header-nav.vue
+++ b/src/web/app/desktop/views/components/ui-header-nav.vue
@@ -76,7 +76,8 @@ export default Vue.extend({
 		},
 
 		messaging() {
-			document.body.appendChild(new MkMessagingWindow().$mount().$el);
+			// TODO
+			//document.body.appendChild(new MkMessagingWindow().$mount().$el);
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/ui-header-search.vue b/src/web/app/desktop/views/components/ui-header-search.vue
index a9cddd8ae..84ca9848c 100644
--- a/src/web/app/desktop/views/components/ui-header-search.vue
+++ b/src/web/app/desktop/views/components/ui-header-search.vue
@@ -1,5 +1,5 @@
 <template>
-<form class="ui-header-search" @submit.prevent="onSubmit">
+<form class="mk-ui-header-search" @submit.prevent="onSubmit">
 	%fa:search%
 	<input v-model="q" type="search" placeholder="%i18n:desktop.tags.mk-ui-header-search.placeholder%"/>
 	<div class="result"></div>
diff --git a/src/web/app/desktop/views/components/ui-notification.vue b/src/web/app/desktop/views/components/ui-notification.vue
index f240037d0..6ca0cebfa 100644
--- a/src/web/app/desktop/views/components/ui-notification.vue
+++ b/src/web/app/desktop/views/components/ui-notification.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-ui-notification">
 	<p>{{ message }}</p>
-<div>
+</div>
 </template>
 
 <script lang="ts">

From 143088b9ef22926ae28233038bd2575bbc44c964 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 08:26:07 +0900
Subject: [PATCH 067/286] wip

---
 src/web/app/desktop/views/components/analog-clock.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/desktop/views/components/analog-clock.vue b/src/web/app/desktop/views/components/analog-clock.vue
index a45bafda6..81eec8159 100644
--- a/src/web/app/desktop/views/components/analog-clock.vue
+++ b/src/web/app/desktop/views/components/analog-clock.vue
@@ -6,7 +6,7 @@
 import Vue from 'vue';
 import { themeColor } from '../../../config';
 
-const Vec2 = function(x, y) {
+const Vec2 = function(this: any, x, y) {
 	this.x = x;
 	this.y = y;
 };

From 21c4f863ef625e54db54ba5cf454b1ca6b50a148 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 08:31:03 +0900
Subject: [PATCH 068/286] wip

---
 src/web/app/desktop/views/components/timeline.vue | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index 933e44825..c580e59f6 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -1,11 +1,11 @@
 <template>
-<div class="mk-timeline" ref="root">
+<div class="mk-timeline">
 	<template v-for="(post, i) in _posts">
 		<mk-timeline-post :post.sync="post" :key="post.id"/>
 		<p class="date" :key="post.id + '-time'" v-if="i != _posts.length - 1 && _post._date != _posts[i + 1]._date"><span>%fa:angle-up%{{ post._datetext }}</span><span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span></p>
 	</template>
-	<footer data-yield="footer">
-		<yield from="footer"/>
+	<footer>
+		<slot name="footer"></slot>
 	</footer>
 </div>
 </template>
@@ -21,7 +21,7 @@ export default Vue.extend({
 		}
 	},
 	computed: {
-		_posts(): any {
+		_posts(): any[] {
 			return this.posts.map(post => {
 				const date = new Date(post.created_at).getDate();
 				const month = new Date(post.created_at).getMonth() + 1;
@@ -36,7 +36,7 @@ export default Vue.extend({
 	},
 	methods: {
 		focus() {
-			(this.$refs.root as any).children[0].focus();
+			(this.$el as any).children[0].focus();
 		}
 	}
 });

From be80c98ee5d536790a07eb753b63479022e9357e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 09:12:54 +0900
Subject: [PATCH 069/286] wip

---
 src/web/app/desktop/-tags/ellipsis-icon.tag   |  37 --
 .../desktop/-tags/home-widgets/timeline.tag   | 143 --------
 src/web/app/desktop/-tags/pages/entrance.tag  | 342 ------------------
 .../views/components/ellipsis-icon.vue        |  37 ++
 src/web/app/desktop/views/components/index.ts |  12 +-
 ...meline-post-sub.vue => posts-post-sub.vue} |   4 +-
 .../{timeline-post.vue => posts-post.vue}     |   6 +-
 .../app/desktop/views/components/posts.vue    |  69 ++++
 .../app/desktop/views/components/timeline.vue | 141 +++++---
 9 files changed, 217 insertions(+), 574 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/ellipsis-icon.tag
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/timeline.tag
 delete mode 100644 src/web/app/desktop/-tags/pages/entrance.tag
 create mode 100644 src/web/app/desktop/views/components/ellipsis-icon.vue
 rename src/web/app/desktop/views/components/{timeline-post-sub.vue => posts-post-sub.vue} (96%)
 rename src/web/app/desktop/views/components/{timeline-post.vue => posts-post.vue} (98%)
 create mode 100644 src/web/app/desktop/views/components/posts.vue

diff --git a/src/web/app/desktop/-tags/ellipsis-icon.tag b/src/web/app/desktop/-tags/ellipsis-icon.tag
deleted file mode 100644
index 619f0d84f..000000000
--- a/src/web/app/desktop/-tags/ellipsis-icon.tag
+++ /dev/null
@@ -1,37 +0,0 @@
-<mk-ellipsis-icon>
-	<div></div>
-	<div></div>
-	<div></div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			width 70px
-			margin 0 auto
-			text-align center
-
-			> div
-				display inline-block
-				width 18px
-				height 18px
-				background-color rgba(0, 0, 0, 0.3)
-				border-radius 100%
-				animation bounce 1.4s infinite ease-in-out both
-
-				&:nth-child(1)
-					animation-delay 0s
-
-				&:nth-child(2)
-					margin 0 6px
-					animation-delay 0.16s
-
-				&:nth-child(3)
-					animation-delay 0.32s
-
-			@keyframes bounce
-				0%, 80%, 100%
-					transform scale(0)
-				40%
-					transform scale(1)
-
-	</style>
-</mk-ellipsis-icon>
diff --git a/src/web/app/desktop/-tags/home-widgets/timeline.tag b/src/web/app/desktop/-tags/home-widgets/timeline.tag
deleted file mode 100644
index 4668ebfa8..000000000
--- a/src/web/app/desktop/-tags/home-widgets/timeline.tag
+++ /dev/null
@@ -1,143 +0,0 @@
-<mk-timeline-home-widget>
-	<mk-following-setuper v-if="noFollowing"/>
-	<div class="loading" v-if="isLoading">
-		<mk-ellipsis-icon/>
-	</div>
-	<p class="empty" v-if="isEmpty && !isLoading">%fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。</p>
-	<mk-timeline ref="timeline" hide={ isLoading }>
-		<yield to="footer">
-			<template v-if="!parent.moreLoading">%fa:moon%</template>
-			<template v-if="parent.moreLoading">%fa:spinner .pulse .fw%</template>
-		</yield/>
-	</mk-timeline>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> mk-following-setuper
-				border-bottom solid 1px #eee
-
-			> .loading
-				padding 64px 0
-
-			> .empty
-				display block
-				margin 0 auto
-				padding 32px
-				max-width 400px
-				text-align center
-				color #999
-
-				> [data-fa]
-					display block
-					margin-bottom 16px
-					font-size 3em
-					color #ccc
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-		this.mixin('api');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.isLoading = true;
-		this.isEmpty = false;
-		this.moreLoading = false;
-		this.noFollowing = this.I.following_count == 0;
-
-		this.on('mount', () => {
-			this.connection.on('post', this.onStreamPost);
-			this.connection.on('follow', this.onStreamFollow);
-			this.connection.on('unfollow', this.onStreamUnfollow);
-
-			document.addEventListener('keydown', this.onDocumentKeydown);
-			window.addEventListener('scroll', this.onScroll);
-
-			this.load(() => this.$emit('loaded'));
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('post', this.onStreamPost);
-			this.connection.off('follow', this.onStreamFollow);
-			this.connection.off('unfollow', this.onStreamUnfollow);
-			this.stream.dispose(this.connectionId);
-
-			document.removeEventListener('keydown', this.onDocumentKeydown);
-			window.removeEventListener('scroll', this.onScroll);
-		});
-
-		this.onDocumentKeydown = e => {
-			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
-				if (e.which == 84) { // t
-					this.$refs.timeline.focus();
-				}
-			}
-		};
-
-		this.load = (cb) => {
-			this.update({
-				isLoading: true
-			});
-
-			this.api('posts/timeline', {
-				until_date: this.date ? this.date.getTime() : undefined
-			}).then(posts => {
-				this.update({
-					isLoading: false,
-					isEmpty: posts.length == 0
-				});
-				this.$refs.timeline.setPosts(posts);
-				if (cb) cb();
-			});
-		};
-
-		this.more = () => {
-			if (this.moreLoading || this.isLoading || this.$refs.timeline.posts.length == 0) return;
-			this.update({
-				moreLoading: true
-			});
-			this.api('posts/timeline', {
-				until_id: this.$refs.timeline.tail().id
-			}).then(posts => {
-				this.update({
-					moreLoading: false
-				});
-				this.$refs.timeline.prependPosts(posts);
-			});
-		};
-
-		this.onStreamPost = post => {
-			this.update({
-				isEmpty: false
-			});
-			this.$refs.timeline.addPost(post);
-		};
-
-		this.onStreamFollow = () => {
-			this.load();
-		};
-
-		this.onStreamUnfollow = () => {
-			this.load();
-		};
-
-		this.onScroll = () => {
-			const current = window.scrollY + window.innerHeight;
-			if (current > document.body.offsetHeight - 8) this.more();
-		};
-
-		this.warp = date => {
-			this.update({
-				date: date
-			});
-
-			this.load();
-		};
-	</script>
-</mk-timeline-home-widget>
diff --git a/src/web/app/desktop/-tags/pages/entrance.tag b/src/web/app/desktop/-tags/pages/entrance.tag
deleted file mode 100644
index 56cec3490..000000000
--- a/src/web/app/desktop/-tags/pages/entrance.tag
+++ /dev/null
@@ -1,342 +0,0 @@
-<mk-entrance>
-	<main>
-		<div>
-			<h1>どこにいても、ここにあります</h1>
-			<p>ようこそ! MisskeyはTwitter風ミニブログSNSです――思ったこと、共有したいことをシンプルに書き残せます。タイムラインを見れば、皆の反応や皆がどう思っているのかもすぐにわかります。</p>
-			<p v-if="stats">これまでに{ stats.posts_count }投稿されました</p>
-		</div>
-		<div>
-			<mk-entrance-signin v-if="mode == 'signin'"/>
-			<mk-entrance-signup v-if="mode == 'signup'"/>
-			<div class="introduction" v-if="mode == 'introduction'">
-				<mk-introduction/>
-				<button @click="signin">わかった</button>
-			</div>
-		</div>
-	</main>
-	<mk-forkit/>
-	<footer>
-		<div>
-			<mk-nav-links/>
-			<p class="c">{ _COPYRIGHT_ }</p>
-		</div>
-	</footer>
-	<!-- ↓ https://github.com/riot/riot/issues/2134 (将来的)-->
-	<style data-disable-scope="data-disable-scope">
-		#wait {
-			right: auto;
-			left: 15px;
-		}
-	</style>
-	<style lang="stylus" scoped>
-		:scope
-			$width = 1000px
-
-			display block
-
-			&:before
-				content ""
-				display block
-				position fixed
-				width 100%
-				height 100%
-				background rgba(0, 0, 0, 0.3)
-
-			> main
-				display block
-				max-width $width
-				margin 0 auto
-				padding 64px 0 0 0
-				padding-bottom 16px
-
-				&:after
-					content ""
-					display block
-					clear both
-
-				> div:first-child
-					position absolute
-					top 64px
-					left 0
-					width calc(100% - 500px)
-					color #fff
-					text-shadow 0 0 32px rgba(0, 0, 0, 0.5)
-					font-weight bold
-
-					> p:last-child
-						padding 1em 0 0 0
-						border-top solid 1px #fff
-
-				> div:last-child
-					float right
-
-					> .introduction
-						max-width 360px
-						margin 0 auto
-						color #777
-
-						> mk-introduction
-							padding 32px
-							background #fff
-							box-shadow 0 4px 16px rgba(0, 0, 0, 0.2)
-
-						> button
-							display block
-							margin 16px auto 0 auto
-							color #666
-
-							&:hover
-								text-decoration underline
-
-			> footer
-				*
-					color #fff !important
-					text-shadow 0 0 8px #000
-					font-weight bold
-
-				> div
-					max-width $width
-					margin 0 auto
-					padding 16px 0
-					text-align center
-					border-top solid 1px #fff
-
-					> .c
-						margin 0
-						line-height 64px
-						font-size 10px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.mode = 'signin';
-
-		this.on('mount', () => {
-			document.documentElement.style.backgroundColor = '#444';
-
-			this.api('meta').then(meta => {
-				const img = meta.top_image ? meta.top_image : '/assets/desktop/index.jpg';
-				document.documentElement.style.backgroundImage = `url("${ img }")`;
-				document.documentElement.style.backgroundSize = 'cover';
-				document.documentElement.style.backgroundPosition = 'center';
-			});
-
-			this.api('stats').then(stats => {
-				this.update({
-					stats
-				});
-			});
-		});
-
-		this.signup = () => {
-			this.update({
-				mode: 'signup'
-			});
-		};
-
-		this.signin = () => {
-			this.update({
-				mode: 'signin'
-			});
-		};
-
-		this.introduction = () => {
-			this.update({
-				mode: 'introduction'
-			});
-		};
-	</script>
-</mk-entrance>
-
-<mk-entrance-signin>
-	<a class="help" href={ _DOCS_URL_ + '/help' } title="お困りですか?">%fa:question%</a>
-	<div class="form">
-		<h1><img v-if="user" src={ user.avatar_url + '?thumbnail&size=32' }/>
-			<p>{ user ? user.name : 'アカウント' }</p>
-		</h1>
-		<mk-signin ref="signin"/>
-	</div>
-	<a href={ _API_URL_ + '/signin/twitter' }>Twitterでサインイン</a>
-	<div class="divider"><span>or</span></div>
-	<button class="signup" @click="parent.signup">新規登録</button><a class="introduction" @click="introduction">Misskeyについて</a>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			width 290px
-			margin 0 auto
-			text-align center
-
-			&:hover
-				> .help
-					opacity 1
-
-			> .help
-				cursor pointer
-				display block
-				position absolute
-				top 0
-				right 0
-				z-index 1
-				margin 0
-				padding 0
-				font-size 1.2em
-				color #999
-				border none
-				outline none
-				background transparent
-				opacity 0
-				transition opacity 0.1s ease
-
-				&:hover
-					color #444
-
-				&:active
-					color #222
-
-				> [data-fa]
-					padding 14px
-
-			> .form
-				padding 10px 28px 16px 28px
-				background #fff
-				box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
-
-				> h1
-					display block
-					margin 0
-					padding 0
-					height 54px
-					line-height 54px
-					text-align center
-					text-transform uppercase
-					font-size 1em
-					font-weight bold
-					color rgba(0, 0, 0, 0.5)
-					border-bottom solid 1px rgba(0, 0, 0, 0.1)
-
-					> p
-						display inline
-						margin 0
-						padding 0
-
-					> img
-						display inline-block
-						top 10px
-						width 32px
-						height 32px
-						margin-right 8px
-						border-radius 100%
-
-						&[src='']
-							display none
-
-			> .divider
-				padding 16px 0
-				text-align center
-
-				&:before
-				&:after
-					content ""
-					display block
-					position absolute
-					top 50%
-					width 45%
-					height 1px
-					border-top solid 1px rgba(0, 0, 0, 0.1)
-
-				&:before
-					left 0
-
-				&:after
-					right 0
-
-				> *
-					z-index 1
-					padding 0 8px
-					color #fff
-					text-shadow 0 0 8px rgba(0, 0, 0, 0.5)
-
-			> .signup
-				width 100%
-				line-height 56px
-				font-size 1em
-				color #fff
-				background $theme-color
-				border-radius 64px
-
-				&:hover
-					background lighten($theme-color, 5%)
-
-				&:active
-					background darken($theme-color, 5%)
-
-			> .introduction
-				display inline-block
-				margin-top 16px
-				font-size 12px
-				color #666
-
-	</style>
-	<script lang="typescript">
-		this.on('mount', () => {
-			this.$refs.signin.on('user', user => {
-				this.update({
-					user: user
-				});
-			});
-		});
-
-		this.introduction = () => {
-			this.parent.introduction();
-		};
-	</script>
-</mk-entrance-signin>
-
-<mk-entrance-signup>
-	<mk-signup/>
-	<button class="cancel" type="button" @click="parent.signin" title="キャンセル">%fa:times%</button>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			width 368px
-			margin 0 auto
-
-			&:hover
-				> .cancel
-					opacity 1
-
-			> mk-signup
-				padding 18px 32px 0 32px
-				background #fff
-				box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
-
-			> .cancel
-				cursor pointer
-				display block
-				position absolute
-				top 0
-				right 0
-				z-index 1
-				margin 0
-				padding 0
-				font-size 1.2em
-				color #999
-				border none
-				outline none
-				box-shadow none
-				background transparent
-				opacity 0
-				transition opacity 0.1s ease
-
-				&:hover
-					color #555
-
-				&:active
-					color #222
-
-				> [data-fa]
-					padding 14px
-
-	</style>
-</mk-entrance-signup>
diff --git a/src/web/app/desktop/views/components/ellipsis-icon.vue b/src/web/app/desktop/views/components/ellipsis-icon.vue
new file mode 100644
index 000000000..c54a7db29
--- /dev/null
+++ b/src/web/app/desktop/views/components/ellipsis-icon.vue
@@ -0,0 +1,37 @@
+<template>
+<div class="mk-ellipsis-icon">
+	<div></div><div></div><div></div>
+</div>
+</template>
+
+<style lang="stylus" scoped>
+.mk-ellipsis-icon
+	width 70px
+	margin 0 auto
+	text-align center
+
+	> div
+		display inline-block
+		width 18px
+		height 18px
+		background-color rgba(0, 0, 0, 0.3)
+		border-radius 100%
+		animation bounce 1.4s infinite ease-in-out both
+
+		&:nth-child(1)
+			animation-delay 0s
+
+		&:nth-child(2)
+			margin 0 6px
+			animation-delay 0.16s
+
+		&:nth-child(3)
+			animation-delay 0.32s
+
+	@keyframes bounce
+		0%, 80%, 100%
+			transform scale(0)
+		40%
+			transform scale(1)
+
+</style>
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 71a049a62..a52953744 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -11,13 +11,15 @@ import uiHeaderSearch from './ui-header-search.vue';
 import uiNotification from './ui-notification.vue';
 import home from './home.vue';
 import timeline from './timeline.vue';
-import timelinePost from './timeline-post.vue';
-import timelinePostSub from './timeline-post-sub.vue';
+import posts from './posts.vue';
+import postsPost from './posts-post.vue';
+import postsPostSub from './posts-post-sub.vue';
 import subPostContent from './sub-post-content.vue';
 import window from './window.vue';
 import postFormWindow from './post-form-window.vue';
 import repostFormWindow from './repost-form-window.vue';
 import analogClock from './analog-clock.vue';
+import ellipsisIcon from './ellipsis-icon.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-header', uiHeader);
@@ -30,10 +32,12 @@ Vue.component('mk-ui-header-search', uiHeaderSearch);
 Vue.component('mk-ui-notification', uiNotification);
 Vue.component('mk-home', home);
 Vue.component('mk-timeline', timeline);
-Vue.component('mk-timeline-post', timelinePost);
-Vue.component('mk-timeline-post-sub', timelinePostSub);
+Vue.component('mk-posts', posts);
+Vue.component('mk-posts-post', postsPost);
+Vue.component('mk-posts-post-sub', postsPostSub);
 Vue.component('mk-sub-post-content', subPostContent);
 Vue.component('mk-window', window);
 Vue.component('mk-post-form-window', postFormWindow);
 Vue.component('mk-repost-form-window', repostFormWindow);
 Vue.component('mk-analog-clock', analogClock);
+Vue.component('mk-ellipsis-icon', ellipsisIcon);
diff --git a/src/web/app/desktop/views/components/timeline-post-sub.vue b/src/web/app/desktop/views/components/posts-post-sub.vue
similarity index 96%
rename from src/web/app/desktop/views/components/timeline-post-sub.vue
rename to src/web/app/desktop/views/components/posts-post-sub.vue
index 120939699..89aeb0482 100644
--- a/src/web/app/desktop/views/components/timeline-post-sub.vue
+++ b/src/web/app/desktop/views/components/posts-post-sub.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-timeline-post-sub" :title="title">
+<div class="mk-posts-post-sub" :title="title">
 	<a class="avatar-anchor" :href="`/${post.user.username}`">
 		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" :v-user-preview="post.user_id"/>
 	</a>
@@ -33,7 +33,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-timeline-post-sub
+.mk-posts-post-sub
 	margin 0
 	padding 0
 	font-size 0.9em
diff --git a/src/web/app/desktop/views/components/timeline-post.vue b/src/web/app/desktop/views/components/posts-post.vue
similarity index 98%
rename from src/web/app/desktop/views/components/timeline-post.vue
rename to src/web/app/desktop/views/components/posts-post.vue
index 6c3d525d5..9991d145e 100644
--- a/src/web/app/desktop/views/components/timeline-post.vue
+++ b/src/web/app/desktop/views/components/posts-post.vue
@@ -1,7 +1,7 @@
 <template>
-<div class="mk-timeline-post" tabindex="-1" :title="title" @keydown="onKeyDown" @dblclick="onDblClick">
+<div class="mk-posts-post" tabindex="-1" :title="title" @keydown="onKeyDown" @dblclick="onDblClick">
 	<div class="reply-to" v-if="p.reply">
-		<mk-timeline-post-sub post="p.reply"/>
+		<mk-posts-post-sub post="p.reply"/>
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
@@ -242,7 +242,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-timeline-post
+.mk-posts-post
 	margin 0
 	padding 0
 	background #fff
diff --git a/src/web/app/desktop/views/components/posts.vue b/src/web/app/desktop/views/components/posts.vue
new file mode 100644
index 000000000..b685bff6a
--- /dev/null
+++ b/src/web/app/desktop/views/components/posts.vue
@@ -0,0 +1,69 @@
+<template>
+<div class="mk-posts">
+	<template v-for="(post, i) in _posts">
+		<mk-posts-post :post.sync="post" :key="post.id"/>
+		<p class="date" :key="post.id + '-time'" v-if="i != _posts.length - 1 && _post._date != _posts[i + 1]._date"><span>%fa:angle-up%{{ post._datetext }}</span><span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span></p>
+	</template>
+	<footer>
+		<slot name="footer"></slot>
+	</footer>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: {
+		posts: {
+			type: Array,
+			default: () => []
+		}
+	},
+	computed: {
+		_posts(): any[] {
+			return (this.posts as any).map(post => {
+				const date = new Date(post.created_at).getDate();
+				const month = new Date(post.created_at).getMonth() + 1;
+				post._date = date;
+				post._datetext = `${month}月 ${date}日`;
+				return post;
+			});
+		}
+	},
+	methods: {
+		focus() {
+			(this.$el as any).children[0].focus();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-posts
+
+	> .date
+		display block
+		margin 0
+		line-height 32px
+		font-size 14px
+		text-align center
+		color #aaa
+		background #fdfdfd
+		border-bottom solid 1px #eaeaea
+
+		span
+			margin 0 16px
+
+		[data-fa]
+			margin-right 8px
+
+	> footer
+		padding 16px
+		text-align center
+		color #ccc
+		border-top solid 1px #eaeaea
+		border-bottom-left-radius 4px
+		border-bottom-right-radius 4px
+
+</style>
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index c580e59f6..b24e78fe4 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -1,12 +1,11 @@
 <template>
 <div class="mk-timeline">
-	<template v-for="(post, i) in _posts">
-		<mk-timeline-post :post.sync="post" :key="post.id"/>
-		<p class="date" :key="post.id + '-time'" v-if="i != _posts.length - 1 && _post._date != _posts[i + 1]._date"><span>%fa:angle-up%{{ post._datetext }}</span><span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span></p>
-	</template>
-	<footer>
-		<slot name="footer"></slot>
-	</footer>
+	<mk-following-setuper v-if="alone"/>
+	<div class="loading" v-if="fetching">
+		<mk-ellipsis-icon/>
+	</div>
+	<p class="empty" v-if="posts.length == 0 && !fetching">%fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。</p>
+	<mk-posts :posts="posts" ref="timeline"/>
 </div>
 </template>
 
@@ -15,28 +14,85 @@ import Vue from 'vue';
 
 export default Vue.extend({
 	props: {
-		posts: {
-			type: Array,
-			default: []
+		date: {
+			type: Date,
+			required: false
 		}
 	},
+	data() {
+		return {
+			fetching: true,
+			moreFetching: false,
+			posts: [],
+			connection: null,
+			connectionId: null
+		};
+	},
 	computed: {
-		_posts(): any[] {
-			return this.posts.map(post => {
-				const date = new Date(post.created_at).getDate();
-				const month = new Date(post.created_at).getMonth() + 1;
-				post._date = date;
-				post._datetext = `${month}月 ${date}日`;
-				return post;
-			});
-		},
-		tail(): any {
-			return this.posts[this.posts.length - 1];
+		alone(): boolean {
+			return this.$root.$data.os.i.following_count == 0;
 		}
 	},
+	mounted() {
+		this.connection = this.$root.$data.os.stream.getConnection();
+		this.connectionId = this.$root.$data.os.stream.use();
+
+		this.connection.on('post', this.onPost);
+		this.connection.on('follow', this.onChangeFollowing);
+		this.connection.on('unfollow', this.onChangeFollowing);
+
+		document.addEventListener('keydown', this.onKeydown);
+		window.addEventListener('scroll', this.onScroll);
+
+		this.fetch();
+	},
+	beforeDestroy() {
+		this.connection.off('post', this.onPost);
+		this.connection.off('follow', this.onChangeFollowing);
+		this.connection.off('unfollow', this.onChangeFollowing);
+		this.$root.$data.os.stream.dispose(this.connectionId);
+
+		document.removeEventListener('keydown', this.onKeydown);
+		window.removeEventListener('scroll', this.onScroll);
+	},
 	methods: {
-		focus() {
-			(this.$el as any).children[0].focus();
+		fetch(cb?) {
+			this.fetching = true;
+
+			this.$root.$data.os.api('posts/timeline', {
+				until_date: this.date ? (this.date as any).getTime() : undefined
+			}).then(posts => {
+				this.fetching = false;
+				this.posts = posts;
+				if (cb) cb();
+			});
+		},
+		more() {
+			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
+			this.moreFetching = true;
+			this.$root.$data.os.api('posts/timeline', {
+				until_id: this.posts[this.posts.length - 1].id
+			}).then(posts => {
+				this.moreFetching = false;
+				this.posts.unshift(posts);
+			});
+		},
+		onPost(post) {
+			this.posts.unshift(post);
+		},
+		onChangeFollowing() {
+			this.fetch();
+		},
+		onScroll() {
+			const current = window.scrollY + window.innerHeight;
+			if (current > document.body.offsetHeight - 8) this.more();
+		},
+		onKeydown(e) {
+			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
+				if (e.which == 84) { // t
+					(this.$refs.timeline as any).focus();
+				}
+			}
 		}
 	}
 });
@@ -44,29 +100,28 @@ export default Vue.extend({
 
 <style lang="stylus" scoped>
 .mk-timeline
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
 
-	> .date
+	> mk-following-setuper
+		border-bottom solid 1px #eee
+
+	> .loading
+		padding 64px 0
+
+	> .empty
 		display block
-		margin 0
-		line-height 32px
-		font-size 14px
+		margin 0 auto
+		padding 32px
+		max-width 400px
 		text-align center
-		color #aaa
-		background #fdfdfd
-		border-bottom solid 1px #eaeaea
+		color #999
 
-		span
-			margin 0 16px
-
-		[data-fa]
-			margin-right 8px
-
-	> footer
-		padding 16px
-		text-align center
-		color #ccc
-		border-top solid 1px #eaeaea
-		border-bottom-left-radius 4px
-		border-bottom-right-radius 4px
+		> [data-fa]
+			display block
+			margin-bottom 16px
+			font-size 3em
+			color #ccc
 
 </style>

From e8affdc730b39ba749f14cd7db05dfd68343e767 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 09:27:57 +0900
Subject: [PATCH 070/286] wip

---
 .../app/common/views/components/post-menu.vue |  2 +-
 .../views/components/reaction-picker.vue      |  2 +-
 .../views/components/stream-indicator.vue     |  2 +-
 src/web/app/desktop/-tags/contextmenu.tag     |  2 +-
 .../desktop/-tags/detailed-post-window.tag    |  2 +-
 src/web/app/desktop/-tags/dialog.tag          |  2 +-
 src/web/app/desktop/-tags/drive/file.tag      |  2 +-
 .../desktop/-tags/home-widgets/slideshow.tag  |  2 +-
 .../app/desktop/-tags/home-widgets/tips.tag   |  2 +-
 src/web/app/desktop/-tags/user-preview.tag    |  2 +-
 .../views/components/images-image-dialog.vue  |  2 +-
 .../desktop/views/components/images-image.vue | 14 ++---
 .../app/desktop/views/components/images.vue   | 54 +++++++++----------
 src/web/app/desktop/views/components/index.ts |  6 +++
 .../app/desktop/views/components/posts.vue    |  2 +-
 .../views/components/ui-notification.vue      |  2 +-
 .../app/desktop/views/components/window.vue   |  2 +-
 src/web/app/mobile/tags/notify.tag            |  2 +-
 18 files changed, 56 insertions(+), 48 deletions(-)

diff --git a/src/web/app/common/views/components/post-menu.vue b/src/web/app/common/views/components/post-menu.vue
index 078e4745a..7a33360f6 100644
--- a/src/web/app/common/views/components/post-menu.vue
+++ b/src/web/app/common/views/components/post-menu.vue
@@ -9,7 +9,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import anime from 'animejs';
+import * as anime from 'animejs';
 
 export default Vue.extend({
 	props: ['post', 'source', 'compact'],
diff --git a/src/web/app/common/views/components/reaction-picker.vue b/src/web/app/common/views/components/reaction-picker.vue
index 62ccbfdd0..b17558ba9 100644
--- a/src/web/app/common/views/components/reaction-picker.vue
+++ b/src/web/app/common/views/components/reaction-picker.vue
@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import anime from 'animejs';
+import * as anime from 'animejs';
 
 const placeholder = '%i18n:common.tags.mk-reaction-picker.choose-reaction%';
 
diff --git a/src/web/app/common/views/components/stream-indicator.vue b/src/web/app/common/views/components/stream-indicator.vue
index 0721c77ad..564376bba 100644
--- a/src/web/app/common/views/components/stream-indicator.vue
+++ b/src/web/app/common/views/components/stream-indicator.vue
@@ -16,7 +16,7 @@
 </template>
 
 <script lang="typescript">
-	import anime from 'animejs';
+	import * as anime from 'animejs';
 	import Ellipsis from './ellipsis.vue';
 
 	export default {
diff --git a/src/web/app/desktop/-tags/contextmenu.tag b/src/web/app/desktop/-tags/contextmenu.tag
index ee4c48fbd..cb9db4f98 100644
--- a/src/web/app/desktop/-tags/contextmenu.tag
+++ b/src/web/app/desktop/-tags/contextmenu.tag
@@ -96,7 +96,7 @@
 
 	</style>
 	<script lang="typescript">
-		import anime from 'animejs';
+		import * as anime from 'animejs';
 		import contains from '../../common/scripts/contains';
 
 		this.root.addEventListener('contextmenu', e => {
diff --git a/src/web/app/desktop/-tags/detailed-post-window.tag b/src/web/app/desktop/-tags/detailed-post-window.tag
index 57e390d50..6803aeacf 100644
--- a/src/web/app/desktop/-tags/detailed-post-window.tag
+++ b/src/web/app/desktop/-tags/detailed-post-window.tag
@@ -35,7 +35,7 @@
 
 	</style>
 	<script lang="typescript">
-		import anime from 'animejs';
+		import * as anime from 'animejs';
 
 		this.mixin('api');
 
diff --git a/src/web/app/desktop/-tags/dialog.tag b/src/web/app/desktop/-tags/dialog.tag
index ba2fa514d..9a486dca5 100644
--- a/src/web/app/desktop/-tags/dialog.tag
+++ b/src/web/app/desktop/-tags/dialog.tag
@@ -83,7 +83,7 @@
 
 	</style>
 	<script lang="typescript">
-		import anime from 'animejs';
+		import * as anime from 'animejs';
 
 		this.canThrough = opts.canThrough != null ? opts.canThrough : true;
 		this.opts.buttons.forEach(button => {
diff --git a/src/web/app/desktop/-tags/drive/file.tag b/src/web/app/desktop/-tags/drive/file.tag
index a669f5fff..153a038f4 100644
--- a/src/web/app/desktop/-tags/drive/file.tag
+++ b/src/web/app/desktop/-tags/drive/file.tag
@@ -141,7 +141,7 @@
 
 	</style>
 	<script lang="typescript">
-		import anime from 'animejs';
+		import * as anime from 'animejs';
 		import bytesToSize from '../../../common/scripts/bytes-to-size';
 
 		this.mixin('i');
diff --git a/src/web/app/desktop/-tags/home-widgets/slideshow.tag b/src/web/app/desktop/-tags/home-widgets/slideshow.tag
index 817b138d3..a69ab74b7 100644
--- a/src/web/app/desktop/-tags/home-widgets/slideshow.tag
+++ b/src/web/app/desktop/-tags/home-widgets/slideshow.tag
@@ -49,7 +49,7 @@
 
 	</style>
 	<script lang="typescript">
-		import anime from 'animejs';
+		import * as anime from 'animejs';
 
 		this.data = {
 			folder: undefined,
diff --git a/src/web/app/desktop/-tags/home-widgets/tips.tag b/src/web/app/desktop/-tags/home-widgets/tips.tag
index a352253ce..efe9c90fc 100644
--- a/src/web/app/desktop/-tags/home-widgets/tips.tag
+++ b/src/web/app/desktop/-tags/home-widgets/tips.tag
@@ -27,7 +27,7 @@
 
 	</style>
 	<script lang="typescript">
-		import anime from 'animejs';
+		import * as anime from 'animejs';
 
 		this.mixin('widget');
 
diff --git a/src/web/app/desktop/-tags/user-preview.tag b/src/web/app/desktop/-tags/user-preview.tag
index 10c37de64..18465c224 100644
--- a/src/web/app/desktop/-tags/user-preview.tag
+++ b/src/web/app/desktop/-tags/user-preview.tag
@@ -99,7 +99,7 @@
 
 	</style>
 	<script lang="typescript">
-		import anime from 'animejs';
+		import * as anime from 'animejs';
 
 		this.mixin('i');
 		this.mixin('api');
diff --git a/src/web/app/desktop/views/components/images-image-dialog.vue b/src/web/app/desktop/views/components/images-image-dialog.vue
index 7975d8061..60afa7af8 100644
--- a/src/web/app/desktop/views/components/images-image-dialog.vue
+++ b/src/web/app/desktop/views/components/images-image-dialog.vue
@@ -7,7 +7,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import anime from 'animejs';
+import * as anime from 'animejs';
 
 export default Vue.extend({
 	props: ['image'],
diff --git a/src/web/app/desktop/views/components/images-image.vue b/src/web/app/desktop/views/components/images-image.vue
index ac662449f..8cb9d5e10 100644
--- a/src/web/app/desktop/views/components/images-image.vue
+++ b/src/web/app/desktop/views/components/images-image.vue
@@ -10,6 +10,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import MkImagesImageDialog from './images-image-dialog.vue';
 
 export default Vue.extend({
 	props: ['image'],
@@ -23,7 +24,7 @@ export default Vue.extend({
 	},
 	methods: {
 		onMousemove(e) {
-			const rect = this.$refs.view.getBoundingClientRect();
+			const rect = this.$el.getBoundingClientRect();
 			const mouseX = e.clientX - rect.left;
 			const mouseY = e.clientY - rect.top;
 			const xp = mouseX / this.$el.offsetWidth * 100;
@@ -36,11 +37,12 @@ export default Vue.extend({
 			this.$el.style.backgroundPosition = '';
 		},
 
-		onClick(ev) {
-			riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), {
-				image: this.image
-			});
-			return false;
+		onClick() {
+			document.body.appendChild(new MkImagesImageDialog({
+				propsData: {
+					image: this.image
+				}
+			}).$mount().$el);
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/images.vue b/src/web/app/desktop/views/components/images.vue
index fb2532753..f02ecbaa8 100644
--- a/src/web/app/desktop/views/components/images.vue
+++ b/src/web/app/desktop/views/components/images.vue
@@ -20,40 +20,40 @@ export default Vue.extend({
 		const tags = this.$refs.image as Vue[];
 
 		if (this.images.length == 1) {
-			this.$el.style.gridTemplateRows = '1fr';
+			(this.$el.style as any).gridTemplateRows = '1fr';
 
-			tags[0].$el.style.gridColumn = '1 / 2';
-			tags[0].$el.style.gridRow = '1 / 2';
+			(tags[0].$el.style as any).gridColumn = '1 / 2';
+			(tags[0].$el.style as any).gridRow = '1 / 2';
 		} else if (this.images.length == 2) {
-			this.$el.style.gridTemplateColumns = '1fr 1fr';
-			this.$el.style.gridTemplateRows = '1fr';
+			(this.$el.style as any).gridTemplateColumns = '1fr 1fr';
+			(this.$el.style as any).gridTemplateRows = '1fr';
 
-			tags[0].$el.style.gridColumn = '1 / 2';
-			tags[0].$el.style.gridRow = '1 / 2';
-			tags[1].$el.style.gridColumn = '2 / 3';
-			tags[1].$el.style.gridRow = '1 / 2';
+			(tags[0].$el.style as any).gridColumn = '1 / 2';
+			(tags[0].$el.style as any).gridRow = '1 / 2';
+			(tags[1].$el.style as any).gridColumn = '2 / 3';
+			(tags[1].$el.style as any).gridRow = '1 / 2';
 		} else if (this.images.length == 3) {
-			this.$el.style.gridTemplateColumns = '1fr 0.5fr';
-			this.$el.style.gridTemplateRows = '1fr 1fr';
+			(this.$el.style as any).gridTemplateColumns = '1fr 0.5fr';
+			(this.$el.style as any).gridTemplateRows = '1fr 1fr';
 
-			tags[0].$el.style.gridColumn = '1 / 2';
-			tags[0].$el.style.gridRow = '1 / 3';
-			tags[1].$el.style.gridColumn = '2 / 3';
-			tags[1].$el.style.gridRow = '1 / 2';
-			tags[2].$el.style.gridColumn = '2 / 3';
-			tags[2].$el.style.gridRow = '2 / 3';
+			(tags[0].$el.style as any).gridColumn = '1 / 2';
+			(tags[0].$el.style as any).gridRow = '1 / 3';
+			(tags[1].$el.style as any).gridColumn = '2 / 3';
+			(tags[1].$el.style as any).gridRow = '1 / 2';
+			(tags[2].$el.style as any).gridColumn = '2 / 3';
+			(tags[2].$el.style as any).gridRow = '2 / 3';
 		} else if (this.images.length == 4) {
-			this.$el.style.gridTemplateColumns = '1fr 1fr';
-			this.$el.style.gridTemplateRows = '1fr 1fr';
+			(this.$el.style as any).gridTemplateColumns = '1fr 1fr';
+			(this.$el.style as any).gridTemplateRows = '1fr 1fr';
 
-			tags[0].$el.style.gridColumn = '1 / 2';
-			tags[0].$el.style.gridRow = '1 / 2';
-			tags[1].$el.style.gridColumn = '2 / 3';
-			tags[1].$el.style.gridRow = '1 / 2';
-			tags[2].$el.style.gridColumn = '1 / 2';
-			tags[2].$el.style.gridRow = '2 / 3';
-			tags[3].$el.style.gridColumn = '2 / 3';
-			tags[3].$el.style.gridRow = '2 / 3';
+			(tags[0].$el.style as any).gridColumn = '1 / 2';
+			(tags[0].$el.style as any).gridRow = '1 / 2';
+			(tags[1].$el.style as any).gridColumn = '2 / 3';
+			(tags[1].$el.style as any).gridRow = '1 / 2';
+			(tags[2].$el.style as any).gridColumn = '1 / 2';
+			(tags[2].$el.style as any).gridRow = '2 / 3';
+			(tags[3].$el.style as any).gridColumn = '2 / 3';
+			(tags[3].$el.style as any).gridRow = '2 / 3';
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index a52953744..f212338e1 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -20,6 +20,9 @@ import postFormWindow from './post-form-window.vue';
 import repostFormWindow from './repost-form-window.vue';
 import analogClock from './analog-clock.vue';
 import ellipsisIcon from './ellipsis-icon.vue';
+import images from './images.vue';
+import imagesImage from './images-image.vue';
+import imagesImageDialog from './images-image-dialog.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-header', uiHeader);
@@ -41,3 +44,6 @@ Vue.component('mk-post-form-window', postFormWindow);
 Vue.component('mk-repost-form-window', repostFormWindow);
 Vue.component('mk-analog-clock', analogClock);
 Vue.component('mk-ellipsis-icon', ellipsisIcon);
+Vue.component('mk-images', images);
+Vue.component('mk-images-image', imagesImage);
+Vue.component('mk-images-image-dialog', imagesImageDialog);
diff --git a/src/web/app/desktop/views/components/posts.vue b/src/web/app/desktop/views/components/posts.vue
index b685bff6a..880ee5224 100644
--- a/src/web/app/desktop/views/components/posts.vue
+++ b/src/web/app/desktop/views/components/posts.vue
@@ -2,7 +2,7 @@
 <div class="mk-posts">
 	<template v-for="(post, i) in _posts">
 		<mk-posts-post :post.sync="post" :key="post.id"/>
-		<p class="date" :key="post.id + '-time'" v-if="i != _posts.length - 1 && _post._date != _posts[i + 1]._date"><span>%fa:angle-up%{{ post._datetext }}</span><span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span></p>
+		<p class="date" :key="post.id + '-time'" v-if="i != _posts.length - 1 && post._date != _posts[i + 1]._date"><span>%fa:angle-up%{{ post._datetext }}</span><span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span></p>
 	</template>
 	<footer>
 		<slot name="footer"></slot>
diff --git a/src/web/app/desktop/views/components/ui-notification.vue b/src/web/app/desktop/views/components/ui-notification.vue
index 6ca0cebfa..6f7b46cb7 100644
--- a/src/web/app/desktop/views/components/ui-notification.vue
+++ b/src/web/app/desktop/views/components/ui-notification.vue
@@ -6,7 +6,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import anime from 'animejs';
+import * as anime from 'animejs';
 
 export default Vue.extend({
 	props: ['message'],
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 986b151c4..61a433b36 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -26,7 +26,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import anime from 'animejs';
+import * as anime from 'animejs';
 import contains from '../../../common/scripts/contains';
 
 const minHeight = 40;
diff --git a/src/web/app/mobile/tags/notify.tag b/src/web/app/mobile/tags/notify.tag
index 59d1e9dd8..ec3609497 100644
--- a/src/web/app/mobile/tags/notify.tag
+++ b/src/web/app/mobile/tags/notify.tag
@@ -16,7 +16,7 @@
 
 	</style>
 	<script lang="typescript">
-		import anime from 'animejs';
+		import * as anime from 'animejs';
 
 		this.on('mount', () => {
 			anime({

From b62f01b2f71c3778defa9eacfbe4eb6ebb55bf9a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 12:06:35 +0900
Subject: [PATCH 071/286] wip

---
 src/web/app/common/-tags/messaging/index.tag  | 456 ------------------
 .../app/common/views/components/messaging.vue | 448 +++++++++++++++++
 2 files changed, 448 insertions(+), 456 deletions(-)
 delete mode 100644 src/web/app/common/-tags/messaging/index.tag
 create mode 100644 src/web/app/common/views/components/messaging.vue

diff --git a/src/web/app/common/-tags/messaging/index.tag b/src/web/app/common/-tags/messaging/index.tag
deleted file mode 100644
index 0432f7e30..000000000
--- a/src/web/app/common/-tags/messaging/index.tag
+++ /dev/null
@@ -1,456 +0,0 @@
-<mk-messaging data-compact={ opts.compact }>
-	<div class="search" v-if="!opts.compact">
-		<div class="form">
-			<label for="search-input">%fa:search%</label>
-			<input ref="search" type="search" oninput={ search } onkeydown={ onSearchKeydown } placeholder="%i18n:common.tags.mk-messaging.search-user%"/>
-		</div>
-		<div class="result">
-			<ol class="users" v-if="searchResult.length > 0" ref="searchResult">
-				<li each={ user, i in searchResult } onkeydown={ parent.onSearchResultKeydown.bind(null, i) } @click="user._click" tabindex="-1">
-					<img class="avatar" src={ user.avatar_url + '?thumbnail&size=32' } alt=""/>
-					<span class="name">{ user.name }</span>
-					<span class="username">@{ user.username }</span>
-				</li>
-			</ol>
-		</div>
-	</div>
-	<div class="history" v-if="history.length > 0">
-		<template each={ history }>
-			<a class="user" data-is-me={ is_me } data-is-read={ is_read } @click="_click">
-				<div>
-					<img class="avatar" src={ (is_me ? recipient.avatar_url : user.avatar_url) + '?thumbnail&size=64' } alt=""/>
-					<header>
-						<span class="name">{ is_me ? recipient.name : user.name }</span>
-						<span class="username">{ '@' + (is_me ? recipient.username : user.username ) }</span>
-						<mk-time time={ created_at }/>
-					</header>
-					<div class="body">
-						<p class="text"><span class="me" v-if="is_me">%i18n:common.tags.mk-messaging.you%:</span>{ text }</p>
-					</div>
-				</div>
-			</a>
-		</template>
-	</div>
-	<p class="no-history" v-if="!fetching && history.length == 0">%i18n:common.tags.mk-messaging.no-history%</p>
-	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			&[data-compact]
-				font-size 0.8em
-
-				> .history
-					> a
-						&:last-child
-							border-bottom none
-
-						&:not([data-is-me]):not([data-is-read])
-							> div
-								background-image none
-								border-left solid 4px #3aa2dc
-
-						> div
-							padding 16px
-
-							> header
-								> mk-time
-									font-size 1em
-
-							> .avatar
-								width 42px
-								height 42px
-								margin 0 12px 0 0
-
-			> .search
-				display block
-				position -webkit-sticky
-				position sticky
-				top 0
-				left 0
-				z-index 1
-				width 100%
-				background #fff
-				box-shadow 0 0px 2px rgba(0, 0, 0, 0.2)
-
-				> .form
-					padding 8px
-					background #f7f7f7
-
-					> label
-						display block
-						position absolute
-						top 0
-						left 8px
-						z-index 1
-						height 100%
-						width 38px
-						pointer-events none
-
-						> [data-fa]
-							display block
-							position absolute
-							top 0
-							right 0
-							bottom 0
-							left 0
-							width 1em
-							height 1em
-							margin auto
-							color #555
-
-					> input
-						margin 0
-						padding 0 0 0 38px
-						width 100%
-						font-size 1em
-						line-height 38px
-						color #000
-						outline none
-						border solid 1px #eee
-						border-radius 5px
-						box-shadow none
-						transition color 0.5s ease, border 0.5s ease
-
-						&:hover
-							border solid 1px #ddd
-							transition border 0.2s ease
-
-						&:focus
-							color darken($theme-color, 20%)
-							border solid 1px $theme-color
-							transition color 0, border 0
-
-				> .result
-					display block
-					top 0
-					left 0
-					z-index 2
-					width 100%
-					margin 0
-					padding 0
-					background #fff
-
-					> .users
-						margin 0
-						padding 0
-						list-style none
-
-						> li
-							display inline-block
-							z-index 1
-							width 100%
-							padding 8px 32px
-							vertical-align top
-							white-space nowrap
-							overflow hidden
-							color rgba(0, 0, 0, 0.8)
-							text-decoration none
-							transition none
-							cursor pointer
-
-							&:hover
-							&:focus
-								color #fff
-								background $theme-color
-
-								.name
-									color #fff
-
-								.username
-									color #fff
-
-							&:active
-								color #fff
-								background darken($theme-color, 10%)
-
-								.name
-									color #fff
-
-								.username
-									color #fff
-
-							.avatar
-								vertical-align middle
-								min-width 32px
-								min-height 32px
-								max-width 32px
-								max-height 32px
-								margin 0 8px 0 0
-								border-radius 6px
-
-							.name
-								margin 0 8px 0 0
-								/*font-weight bold*/
-								font-weight normal
-								color rgba(0, 0, 0, 0.8)
-
-							.username
-								font-weight normal
-								color rgba(0, 0, 0, 0.3)
-
-			> .history
-
-				> a
-					display block
-					text-decoration none
-					background #fff
-					border-bottom solid 1px #eee
-
-					*
-						pointer-events none
-						user-select none
-
-					&:hover
-						background #fafafa
-
-						> .avatar
-							filter saturate(200%)
-
-					&:active
-						background #eee
-
-					&[data-is-read]
-					&[data-is-me]
-						opacity 0.8
-
-					&:not([data-is-me]):not([data-is-read])
-						> div
-							background-image url("/assets/unread.svg")
-							background-repeat no-repeat
-							background-position 0 center
-
-					&:after
-						content ""
-						display block
-						clear both
-
-					> div
-						max-width 500px
-						margin 0 auto
-						padding 20px 30px
-
-						&:after
-							content ""
-							display block
-							clear both
-
-						> header
-							margin-bottom 2px
-							white-space nowrap
-							overflow hidden
-
-							> .name
-								text-align left
-								display inline
-								margin 0
-								padding 0
-								font-size 1em
-								color rgba(0, 0, 0, 0.9)
-								font-weight bold
-								transition all 0.1s ease
-
-							> .username
-								text-align left
-								margin 0 0 0 8px
-								color rgba(0, 0, 0, 0.5)
-
-							> mk-time
-								position absolute
-								top 0
-								right 0
-								display inline
-								color rgba(0, 0, 0, 0.5)
-								font-size 80%
-
-						> .avatar
-							float left
-							width 54px
-							height 54px
-							margin 0 16px 0 0
-							border-radius 8px
-							transition all 0.1s ease
-
-						> .body
-
-							> .text
-								display block
-								margin 0 0 0 0
-								padding 0
-								overflow hidden
-								overflow-wrap break-word
-								font-size 1.1em
-								color rgba(0, 0, 0, 0.8)
-
-								.me
-									color rgba(0, 0, 0, 0.4)
-
-							> .image
-								display block
-								max-width 100%
-								max-height 512px
-
-			> .no-history
-				margin 0
-				padding 2em 1em
-				text-align center
-				color #999
-				font-weight 500
-
-			> .fetching
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-			// TODO: element base media query
-			@media (max-width 400px)
-				> .search
-					> .result
-						> .users
-							> li
-								padding 8px 16px
-
-				> .history
-					> a
-						&:not([data-is-me]):not([data-is-read])
-							> div
-								background-image none
-								border-left solid 4px #3aa2dc
-
-						> div
-							padding 16px
-							font-size 14px
-
-							> .avatar
-								margin 0 12px 0 0
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-		this.mixin('api');
-
-		this.mixin('messaging-index-stream');
-		this.connection = this.messagingIndexStream.getConnection();
-		this.connectionId = this.messagingIndexStream.use();
-
-		this.searchResult = [];
-		this.history = [];
-		this.fetching = true;
-
-		this.registerMessage = message => {
-			message.is_me = message.user_id == this.I.id;
-			message._click = () => {
-				this.$emit('navigate-user', message.is_me ? message.recipient : message.user);
-			};
-		};
-
-		this.on('mount', () => {
-			this.connection.on('message', this.onMessage);
-			this.connection.on('read', this.onRead);
-
-			this.api('messaging/history').then(history => {
-				this.fetching = false;
-				history.forEach(message => {
-					this.registerMessage(message);
-				});
-				this.history = history;
-				this.update();
-			});
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('message', this.onMessage);
-			this.connection.off('read', this.onRead);
-			this.messagingIndexStream.dispose(this.connectionId);
-		});
-
-		this.onMessage = message => {
-			this.history = this.history.filter(m => !(
-				(m.recipient_id == message.recipient_id && m.user_id == message.user_id) ||
-				(m.recipient_id == message.user_id && m.user_id == message.recipient_id)));
-
-			this.registerMessage(message);
-
-			this.history.unshift(message);
-			this.update();
-		};
-
-		this.onRead = ids => {
-			ids.forEach(id => {
-				const found = this.history.find(m => m.id == id);
-				if (found) found.is_read = true;
-			});
-
-			this.update();
-		};
-
-		this.search = () => {
-			const q = this.$refs.search.value;
-			if (q == '') {
-				this.searchResult = [];
-				return;
-			}
-			this.api('users/search', {
-				query: q,
-				max: 5
-			}).then(users => {
-				users.forEach(user => {
-					user._click = () => {
-						this.$emit('navigate-user', user);
-						this.searchResult = [];
-					};
-				});
-				this.update({
-					searchResult: users
-				});
-			});
-		};
-
-		this.onSearchKeydown = e => {
-			switch (e.which) {
-				case 9: // [TAB]
-				case 40: // [↓]
-					e.preventDefault();
-					e.stopPropagation();
-					this.$refs.searchResult.childNodes[0].focus();
-					break;
-			}
-		};
-
-		this.onSearchResultKeydown = (i, e) => {
-			const cancel = () => {
-				e.preventDefault();
-				e.stopPropagation();
-			};
-			switch (true) {
-				case e.which == 10: // [ENTER]
-				case e.which == 13: // [ENTER]
-					cancel();
-					this.searchResult[i]._click();
-					break;
-
-				case e.which == 27: // [ESC]
-					cancel();
-					this.$refs.search.focus();
-					break;
-
-				case e.which == 9 && e.shiftKey: // [TAB] + [Shift]
-				case e.which == 38: // [↑]
-					cancel();
-					(this.$refs.searchResult.childNodes[i].previousElementSibling || this.$refs.searchResult.childNodes[this.searchResult.length - 1]).focus();
-					break;
-
-				case e.which == 9: // [TAB]
-				case e.which == 40: // [↓]
-					cancel();
-					(this.$refs.searchResult.childNodes[i].nextElementSibling || this.$refs.searchResult.childNodes[0]).focus();
-					break;
-			}
-		};
-
-	</script>
-</mk-messaging>
diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
new file mode 100644
index 000000000..2e81325cb
--- /dev/null
+++ b/src/web/app/common/views/components/messaging.vue
@@ -0,0 +1,448 @@
+<template>
+<div class="mk-messaging" :data-compact="compact">
+	<div class="search" v-if="!opts.compact">
+		<div class="form">
+			<label for="search-input">%fa:search%</label>
+			<input v-model="q" type="search" @input="search" @keydown="onSearchKeydown" placeholder="%i18n:common.tags.mk-messaging.search-user%"/>
+		</div>
+		<div class="result">
+			<ol class="users" v-if="searchResult.length > 0" ref="searchResult">
+				<li each={ user, i in searchResult }
+					@keydown.enter="navigate(user)"
+					onkeydown={ parent.onSearchResultKeydown.bind(null, i) }
+					@click="user._click"
+					tabindex="-1"
+				>
+					<img class="avatar" src={ user.avatar_url + '?thumbnail&size=32' } alt=""/>
+					<span class="name">{ user.name }</span>
+					<span class="username">@{ user.username }</span>
+				</li>
+			</ol>
+		</div>
+	</div>
+	<div class="history" v-if="history.length > 0">
+		<template each={ history }>
+			<a class="user" data-is-me={ is_me } data-is-read={ is_read } @click="navigate(isMe(message) ? message.recipient : message.user)">
+				<div>
+					<img class="avatar" src={ (is_me ? recipient.avatar_url : user.avatar_url) + '?thumbnail&size=64' } alt=""/>
+					<header>
+						<span class="name">{ is_me ? recipient.name : user.name }</span>
+						<span class="username">{ '@' + (is_me ? recipient.username : user.username ) }</span>
+						<mk-time time={ created_at }/>
+					</header>
+					<div class="body">
+						<p class="text"><span class="me" v-if="is_me">%i18n:common.tags.mk-messaging.you%:</span>{ text }</p>
+					</div>
+				</div>
+			</a>
+		</template>
+	</div>
+	<p class="no-history" v-if="!fetching && history.length == 0">%i18n:common.tags.mk-messaging.no-history%</p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: {
+		compact: {
+			type: Boolean,
+			default: false
+		}
+	},
+	data() {
+		return {
+			fetching: true,
+			moreFetching: false,
+			messages: [],
+			q: null,
+			result: [],
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		this.connection = this.$root.$data.os.streams.messagingIndexStream.getConnection();
+		this.connectionId = this.$root.$data.os.streams.messagingIndexStream.use();
+
+		this.connection.on('message', this.onMessage);
+		this.connection.on('read', this.onRead);
+
+		this.$root.$data.os.api('messaging/history').then(messages => {
+			this.fetching = false;
+			this.messages = messages;
+		});
+	},
+	beforeDestroy() {
+		this.connection.off('message', this.onMessage);
+		this.connection.off('read', this.onRead);
+		this.$root.$data.os.stream.dispose(this.connectionId);
+	},
+	methods: {
+		isMe(message) {
+			return message.user_id == this.$root.$data.os.i.id;
+		},
+		onMessage(message) {
+			this.messages = this.messages.filter(m => !(
+				(m.recipient_id == message.recipient_id && m.user_id == message.user_id) ||
+				(m.recipient_id == message.user_id && m.user_id == message.recipient_id)));
+
+			this.messages.unshift(message);
+		},
+		onRead(ids) {
+			ids.forEach(id => {
+				const found = this.messages.find(m => m.id == id);
+				if (found) found.is_read = true;
+			});
+		},
+		search() {
+			if (this.q == '') {
+				this.result = [];
+				return;
+			}
+			this.$root.$data.os.api('users/search', {
+				query: this.q,
+				max: 5
+			}).then(users => {
+				this.result = users;
+			});
+		},
+		navigate(user) {
+			this.$emit('navigate', user);
+		},
+		onSearchKeydown(e) {
+			switch (e.which) {
+				case 9: // [TAB]
+				case 40: // [↓]
+					e.preventDefault();
+					e.stopPropagation();
+					(this.$refs.searchResult as any).childNodes[0].focus();
+					break;
+			}
+		},
+		onSearchResultKeydown(i, e) {
+			const cancel = () => {
+				e.preventDefault();
+				e.stopPropagation();
+			};
+			switch (true) {
+				case e.which == 27: // [ESC]
+					cancel();
+					this.$refs.search.focus();
+					break;
+
+				case e.which == 9 && e.shiftKey: // [TAB] + [Shift]
+				case e.which == 38: // [↑]
+					cancel();
+					(this.$refs.searchResult.childNodes[i].previousElementSibling || this.$refs.searchResult.childNodes[this.searchResult.length - 1]).focus();
+					break;
+
+				case e.which == 9: // [TAB]
+				case e.which == 40: // [↓]
+					cancel();
+					(this.$refs.searchResult.childNodes[i].nextElementSibling || this.$refs.searchResult.childNodes[0]).focus();
+					break;
+			}
+		}
+	}
+});
+</script>
+
+
+<style lang="stylus" scoped>
+.mk-messaging
+
+	&[data-compact]
+		font-size 0.8em
+
+		> .history
+			> a
+				&:last-child
+					border-bottom none
+
+				&:not([data-is-me]):not([data-is-read])
+					> div
+						background-image none
+						border-left solid 4px #3aa2dc
+
+				> div
+					padding 16px
+
+					> header
+						> mk-time
+							font-size 1em
+
+					> .avatar
+						width 42px
+						height 42px
+						margin 0 12px 0 0
+
+	> .search
+		display block
+		position -webkit-sticky
+		position sticky
+		top 0
+		left 0
+		z-index 1
+		width 100%
+		background #fff
+		box-shadow 0 0px 2px rgba(0, 0, 0, 0.2)
+
+		> .form
+			padding 8px
+			background #f7f7f7
+
+			> label
+				display block
+				position absolute
+				top 0
+				left 8px
+				z-index 1
+				height 100%
+				width 38px
+				pointer-events none
+
+				> [data-fa]
+					display block
+					position absolute
+					top 0
+					right 0
+					bottom 0
+					left 0
+					width 1em
+					height 1em
+					margin auto
+					color #555
+
+			> input
+				margin 0
+				padding 0 0 0 38px
+				width 100%
+				font-size 1em
+				line-height 38px
+				color #000
+				outline none
+				border solid 1px #eee
+				border-radius 5px
+				box-shadow none
+				transition color 0.5s ease, border 0.5s ease
+
+				&:hover
+					border solid 1px #ddd
+					transition border 0.2s ease
+
+				&:focus
+					color darken($theme-color, 20%)
+					border solid 1px $theme-color
+					transition color 0, border 0
+
+		> .result
+			display block
+			top 0
+			left 0
+			z-index 2
+			width 100%
+			margin 0
+			padding 0
+			background #fff
+
+			> .users
+				margin 0
+				padding 0
+				list-style none
+
+				> li
+					display inline-block
+					z-index 1
+					width 100%
+					padding 8px 32px
+					vertical-align top
+					white-space nowrap
+					overflow hidden
+					color rgba(0, 0, 0, 0.8)
+					text-decoration none
+					transition none
+					cursor pointer
+
+					&:hover
+					&:focus
+						color #fff
+						background $theme-color
+
+						.name
+							color #fff
+
+						.username
+							color #fff
+
+					&:active
+						color #fff
+						background darken($theme-color, 10%)
+
+						.name
+							color #fff
+
+						.username
+							color #fff
+
+					.avatar
+						vertical-align middle
+						min-width 32px
+						min-height 32px
+						max-width 32px
+						max-height 32px
+						margin 0 8px 0 0
+						border-radius 6px
+
+					.name
+						margin 0 8px 0 0
+						/*font-weight bold*/
+						font-weight normal
+						color rgba(0, 0, 0, 0.8)
+
+					.username
+						font-weight normal
+						color rgba(0, 0, 0, 0.3)
+
+	> .history
+
+		> a
+			display block
+			text-decoration none
+			background #fff
+			border-bottom solid 1px #eee
+
+			*
+				pointer-events none
+				user-select none
+
+			&:hover
+				background #fafafa
+
+				> .avatar
+					filter saturate(200%)
+
+			&:active
+				background #eee
+
+			&[data-is-read]
+			&[data-is-me]
+				opacity 0.8
+
+			&:not([data-is-me]):not([data-is-read])
+				> div
+					background-image url("/assets/unread.svg")
+					background-repeat no-repeat
+					background-position 0 center
+
+			&:after
+				content ""
+				display block
+				clear both
+
+			> div
+				max-width 500px
+				margin 0 auto
+				padding 20px 30px
+
+				&:after
+					content ""
+					display block
+					clear both
+
+				> header
+					margin-bottom 2px
+					white-space nowrap
+					overflow hidden
+
+					> .name
+						text-align left
+						display inline
+						margin 0
+						padding 0
+						font-size 1em
+						color rgba(0, 0, 0, 0.9)
+						font-weight bold
+						transition all 0.1s ease
+
+					> .username
+						text-align left
+						margin 0 0 0 8px
+						color rgba(0, 0, 0, 0.5)
+
+					> mk-time
+						position absolute
+						top 0
+						right 0
+						display inline
+						color rgba(0, 0, 0, 0.5)
+						font-size 80%
+
+				> .avatar
+					float left
+					width 54px
+					height 54px
+					margin 0 16px 0 0
+					border-radius 8px
+					transition all 0.1s ease
+
+				> .body
+
+					> .text
+						display block
+						margin 0 0 0 0
+						padding 0
+						overflow hidden
+						overflow-wrap break-word
+						font-size 1.1em
+						color rgba(0, 0, 0, 0.8)
+
+						.me
+							color rgba(0, 0, 0, 0.4)
+
+					> .image
+						display block
+						max-width 100%
+						max-height 512px
+
+	> .no-history
+		margin 0
+		padding 2em 1em
+		text-align center
+		color #999
+		font-weight 500
+
+	> .fetching
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+	// TODO: element base media query
+	@media (max-width 400px)
+		> .search
+			> .result
+				> .users
+					> li
+						padding 8px 16px
+
+		> .history
+			> a
+				&:not([data-is-me]):not([data-is-read])
+					> div
+						background-image none
+						border-left solid 4px #3aa2dc
+
+				> div
+					padding 16px
+					font-size 14px
+
+					> .avatar
+						margin 0 12px 0 0
+
+</style>

From 1136457fb811b0fd192df9474f2919228a714305 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 12:21:02 +0900
Subject: [PATCH 072/286] wip

---
 .../app/common/views/components/messaging.vue | 49 +++++++++++--------
 1 file changed, 29 insertions(+), 20 deletions(-)

diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index 2e81325cb..386e705b0 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -6,38 +6,45 @@
 			<input v-model="q" type="search" @input="search" @keydown="onSearchKeydown" placeholder="%i18n:common.tags.mk-messaging.search-user%"/>
 		</div>
 		<div class="result">
-			<ol class="users" v-if="searchResult.length > 0" ref="searchResult">
-				<li each={ user, i in searchResult }
+			<ol class="users" v-if="result.length > 0" ref="searchResult">
+				<li v-for="(user, i) in result"
 					@keydown.enter="navigate(user)"
-					onkeydown={ parent.onSearchResultKeydown.bind(null, i) }
-					@click="user._click"
+					@keydown="onSearchResultKeydown(i)"
+					@click="navigate(user)"
 					tabindex="-1"
+					:key="user.id"
 				>
-					<img class="avatar" src={ user.avatar_url + '?thumbnail&size=32' } alt=""/>
-					<span class="name">{ user.name }</span>
-					<span class="username">@{ user.username }</span>
+					<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=32`" alt=""/>
+					<span class="name">{{ user.name }}</span>
+					<span class="username">@{{ user.username }}</span>
 				</li>
 			</ol>
 		</div>
 	</div>
-	<div class="history" v-if="history.length > 0">
-		<template each={ history }>
-			<a class="user" data-is-me={ is_me } data-is-read={ is_read } @click="navigate(isMe(message) ? message.recipient : message.user)">
+	<div class="history" v-if="messages.length > 0">
+		<template >
+			<a v-for="message in messages"
+				class="user"
+				:data-is-me="isMe(message)"
+				:data-is-read="message.is_read"
+				@click="navigate(isMe(message) ? message.recipient : message.user)"
+				:key="message.id"
+			>
 				<div>
-					<img class="avatar" src={ (is_me ? recipient.avatar_url : user.avatar_url) + '?thumbnail&size=64' } alt=""/>
+					<img class="avatar" :src="`${isMe(message) ? message.recipient.avatar_url : message.user.avatar_url}?thumbnail&size=64`" alt=""/>
 					<header>
-						<span class="name">{ is_me ? recipient.name : user.name }</span>
-						<span class="username">{ '@' + (is_me ? recipient.username : user.username ) }</span>
-						<mk-time time={ created_at }/>
+						<span class="name">{{ isMe(message) ? message.recipient.name : message.user.name }}</span>
+						<span class="username">@{{ isMe(message) ? message.recipient.username : message.user.username }}</span>
+						<mk-time :time="message.created_at"/>
 					</header>
 					<div class="body">
-						<p class="text"><span class="me" v-if="is_me">%i18n:common.tags.mk-messaging.you%:</span>{ text }</p>
+						<p class="text"><span class="me" v-if="isMe(message)">%i18n:common.tags.mk-messaging.you%:</span>{{ text }}</p>
 					</div>
 				</div>
 			</a>
 		</template>
 	</div>
-	<p class="no-history" v-if="!fetching && history.length == 0">%i18n:common.tags.mk-messaging.no-history%</p>
+	<p class="no-history" v-if="!fetching && messages.length == 0">%i18n:common.tags.mk-messaging.no-history%</p>
 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 </div>
 </template>
@@ -123,26 +130,29 @@ export default Vue.extend({
 			}
 		},
 		onSearchResultKeydown(i, e) {
+			const list = this.$refs.searchResult as any;
+
 			const cancel = () => {
 				e.preventDefault();
 				e.stopPropagation();
 			};
+
 			switch (true) {
 				case e.which == 27: // [ESC]
 					cancel();
-					this.$refs.search.focus();
+					(this.$refs.search as any).focus();
 					break;
 
 				case e.which == 9 && e.shiftKey: // [TAB] + [Shift]
 				case e.which == 38: // [↑]
 					cancel();
-					(this.$refs.searchResult.childNodes[i].previousElementSibling || this.$refs.searchResult.childNodes[this.searchResult.length - 1]).focus();
+					(list.childNodes[i].previousElementSibling || list.childNodes[this.result.length - 1]).focus();
 					break;
 
 				case e.which == 9: // [TAB]
 				case e.which == 40: // [↓]
 					cancel();
-					(this.$refs.searchResult.childNodes[i].nextElementSibling || this.$refs.searchResult.childNodes[0]).focus();
+					(list.childNodes[i].nextElementSibling || list.childNodes[0]).focus();
 					break;
 			}
 		}
@@ -150,7 +160,6 @@ export default Vue.extend({
 });
 </script>
 
-
 <style lang="stylus" scoped>
 .mk-messaging
 

From e1cbaf2c5a2e14d64e85556b15b9b45ca044ec3e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 12:32:00 +0900
Subject: [PATCH 073/286] wip

---
 src/web/app/common/views/components/index.ts          |  2 ++
 src/web/app/common/views/components/reaction-icon.vue | 10 +++++++++-
 src/web/app/desktop/views/components/posts-post.vue   |  4 ++--
 3 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index c4c3475ee..26213297a 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -5,9 +5,11 @@ import signup from './signup.vue';
 import forkit from './forkit.vue';
 import nav from './nav.vue';
 import postHtml from './post-html';
+import reactionIcon from './reaction-icon.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
 Vue.component('mk-forkit', forkit);
 Vue.component('mk-nav', nav);
 Vue.component('mk-post-html', postHtml);
+Vue.component('mk-reaction-icon', reactionIcon);
diff --git a/src/web/app/common/views/components/reaction-icon.vue b/src/web/app/common/views/components/reaction-icon.vue
index 317daf0fe..7d24f4f9e 100644
--- a/src/web/app/common/views/components/reaction-icon.vue
+++ b/src/web/app/common/views/components/reaction-icon.vue
@@ -1,5 +1,5 @@
 <template>
-<span>
+<span class="mk-reaction-icon">
 	<img v-if="reaction == 'like'" src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%">
 	<img v-if="reaction == 'love'" src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%">
 	<img v-if="reaction == 'laugh'" src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%">
@@ -12,7 +12,15 @@
 </span>
 </template>
 
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['reaction']
+});
+</script>
+
 <style lang="stylus" scoped>
+.mk-reaction-icon
 	img
 		vertical-align middle
 		width 1em
diff --git a/src/web/app/desktop/views/components/posts-post.vue b/src/web/app/desktop/views/components/posts-post.vue
index 9991d145e..cc2d7534a 100644
--- a/src/web/app/desktop/views/components/posts-post.vue
+++ b/src/web/app/desktop/views/components/posts-post.vue
@@ -24,7 +24,7 @@
 				<div class="info">
 					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
 					<a class="created-at" :href="url">
-						<mk-time time="p.created_at"/>
+						<mk-time :time="p.created_at"/>
 					</a>
 				</div>
 			</header>
@@ -188,7 +188,7 @@ export default Vue.extend({
 		react() {
 			document.body.appendChild(new MkReactionPicker({
 				propsData: {
-					source: this.$refs.menuButton,
+					source: this.$refs.reactButton,
 					post: this.p
 				}
 			}).$mount().$el);

From cbdc06ad71f73ff7a847eeca2f4f9da926a5af85 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 12:50:42 +0900
Subject: [PATCH 074/286] wip

---
 .../views/components/reaction-picker.vue      | 56 ++++++++++---------
 .../desktop/views/components/posts-post.vue   |  4 +-
 2 files changed, 31 insertions(+), 29 deletions(-)

diff --git a/src/web/app/common/views/components/reaction-picker.vue b/src/web/app/common/views/components/reaction-picker.vue
index b17558ba9..0446d7b18 100644
--- a/src/web/app/common/views/components/reaction-picker.vue
+++ b/src/web/app/common/views/components/reaction-picker.vue
@@ -32,36 +32,38 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		const popover = this.$refs.popover as any;
+		this.$nextTick(() => {
+			const popover = this.$refs.popover as any;
 
-		const rect = this.source.getBoundingClientRect();
-		const width = popover.offsetWidth;
-		const height = popover.offsetHeight;
+			const rect = this.source.getBoundingClientRect();
+			const width = popover.offsetWidth;
+			const height = popover.offsetHeight;
 
-		if (this.compact) {
-			const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-			const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
-			popover.style.left = (x - (width / 2)) + 'px';
-			popover.style.top = (y - (height / 2)) + 'px';
-		} else {
-			const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-			const y = rect.top + window.pageYOffset + this.source.offsetHeight;
-			popover.style.left = (x - (width / 2)) + 'px';
-			popover.style.top = y + 'px';
-		}
+			if (this.compact) {
+				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+				const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
+				popover.style.left = (x - (width / 2)) + 'px';
+				popover.style.top = (y - (height / 2)) + 'px';
+			} else {
+				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+				const y = rect.top + window.pageYOffset + this.source.offsetHeight;
+				popover.style.left = (x - (width / 2)) + 'px';
+				popover.style.top = y + 'px';
+			}
 
-		anime({
-			targets: this.$refs.backdrop,
-			opacity: 1,
-			duration: 100,
-			easing: 'linear'
-		});
+			anime({
+				targets: this.$refs.backdrop,
+				opacity: 1,
+				duration: 100,
+				easing: 'linear'
+			});
 
-		anime({
-			targets: this.$refs.popover,
-			opacity: 1,
-			scale: [0.5, 1],
-			duration: 500
+			anime({
+				targets: this.$refs.popover,
+				opacity: 1,
+				scale: [0.5, 1],
+				duration: 500
+			});
 		});
 	},
 	methods: {
@@ -104,7 +106,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-	$border-color = rgba(27, 31, 35, 0.15)
+$border-color = rgba(27, 31, 35, 0.15)
 
 .mk-reaction-picker
 	position initial
diff --git a/src/web/app/desktop/views/components/posts-post.vue b/src/web/app/desktop/views/components/posts-post.vue
index cc2d7534a..2633a63f2 100644
--- a/src/web/app/desktop/views/components/posts-post.vue
+++ b/src/web/app/desktop/views/components/posts-post.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-posts-post" tabindex="-1" :title="title" @keydown="onKeyDown" @dblclick="onDblClick">
+<div class="mk-posts-post" tabindex="-1" :title="title" @keydown="onKeydown" @dblclick="onDblClick">
 	<div class="reply-to" v-if="p.reply">
 		<mk-posts-post-sub post="p.reply"/>
 	</div>
@@ -32,7 +32,7 @@
 				<div class="text" ref="text">
 					<p class="channel" v-if="p.channel"><a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:</p>
 					<a class="reply" v-if="p.reply">%fa:reply%</a>
-					<mk-post-html :ast="p.ast" :i="$root.$data.os.i"/>
+					<mk-post-html v-if="p.ast" :ast="p.ast" :i="$root.$data.os.i"/>
 					<a class="quote" v-if="p.repost">RP:</a>
 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 				</div>

From c775e7d9659e99db196c956b2f224ec33b8bbaeb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 13:18:03 +0900
Subject: [PATCH 075/286] wip

---
 package.json                            |  1 +
 webpack/module/rules/base64.ts          |  2 +-
 webpack/module/rules/collapse-spaces.ts | 20 ++++++++++++++++++++
 webpack/module/rules/index.ts           |  2 ++
 4 files changed, 24 insertions(+), 1 deletion(-)
 create mode 100644 webpack/module/rules/collapse-spaces.ts

diff --git a/package.json b/package.json
index fee512c7f..906d512dc 100644
--- a/package.json
+++ b/package.json
@@ -118,6 +118,7 @@
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
 		"highlight.js": "9.12.0",
+		"html-minifier": "^3.5.9",
 		"inquirer": "5.0.1",
 		"is-root": "1.0.0",
 		"is-url": "1.2.2",
diff --git a/webpack/module/rules/base64.ts b/webpack/module/rules/base64.ts
index 529816bd2..6d7eaddeb 100644
--- a/webpack/module/rules/base64.ts
+++ b/webpack/module/rules/base64.ts
@@ -7,7 +7,7 @@ const StringReplacePlugin = require('string-replace-webpack-plugin');
 
 export default () => ({
 	enforce: 'pre',
-	test: /\.(tag|js)$/,
+	test: /\.(vue|js)$/,
 	exclude: /node_modules/,
 	loader: StringReplacePlugin.replace({
 		replacements: [{
diff --git a/webpack/module/rules/collapse-spaces.ts b/webpack/module/rules/collapse-spaces.ts
new file mode 100644
index 000000000..48fd57f01
--- /dev/null
+++ b/webpack/module/rules/collapse-spaces.ts
@@ -0,0 +1,20 @@
+import * as fs from 'fs';
+const minify = require('html-minifier').minify;
+const StringReplacePlugin = require('string-replace-webpack-plugin');
+
+export default () => ({
+	enforce: 'pre',
+	test: /\.vue$/,
+	exclude: /node_modules/,
+	loader: StringReplacePlugin.replace({
+		replacements: [{
+			pattern: /^<template>([\s\S]+?)\r?\n<\/template>/, replacement: html => {
+				return minify(html, {
+					collapseWhitespace: true,
+					collapseInlineTagWhitespace: true,
+					keepClosingSlash: true
+				});
+			}
+		}]
+	})
+});
diff --git a/webpack/module/rules/index.ts b/webpack/module/rules/index.ts
index 093f07330..c63da7112 100644
--- a/webpack/module/rules/index.ts
+++ b/webpack/module/rules/index.ts
@@ -6,8 +6,10 @@ import themeColor from './theme-color';
 import vue from './vue';
 import stylus from './stylus';
 import typescript from './typescript';
+import collapseSpaces from './collapse-spaces';
 
 export default lang => [
+	collapseSpaces(),
 	i18n(lang),
 	license(),
 	fa(),

From f789f111c12c5347a031f77b4a93549cc8125b05 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 13:49:48 +0900
Subject: [PATCH 076/286] wip

---
 src/web/app/common/views/components/index.ts  |   2 +
 src/web/app/common/views/components/time.vue  | 125 ++++++++++--------
 .../desktop/views/components/images-image.vue |   2 +-
 .../desktop/views/components/posts-post.vue   |  11 +-
 4 files changed, 80 insertions(+), 60 deletions(-)

diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 26213297a..3d78e7f9c 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -6,6 +6,7 @@ import forkit from './forkit.vue';
 import nav from './nav.vue';
 import postHtml from './post-html';
 import reactionIcon from './reaction-icon.vue';
+import time from './time.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
@@ -13,3 +14,4 @@ Vue.component('mk-forkit', forkit);
 Vue.component('mk-nav', nav);
 Vue.component('mk-post-html', postHtml);
 Vue.component('mk-reaction-icon', reactionIcon);
+Vue.component('mk-time', time);
diff --git a/src/web/app/common/views/components/time.vue b/src/web/app/common/views/components/time.vue
index 7d165fc00..3c856d3f2 100644
--- a/src/web/app/common/views/components/time.vue
+++ b/src/web/app/common/views/components/time.vue
@@ -1,63 +1,76 @@
 <template>
-	<time>
-		<span v-if=" mode == 'relative' ">{{ relative }}</span>
-		<span v-if=" mode == 'absolute' ">{{ absolute }}</span>
-		<span v-if=" mode == 'detail' ">{{ absolute }} ({{ relative }})</span>
-	</time>
+<time>
+	<span v-if=" mode == 'relative' ">{{ relative }}</span>
+	<span v-if=" mode == 'absolute' ">{{ absolute }}</span>
+	<span v-if=" mode == 'detail' ">{{ absolute }} ({{ relative }})</span>
+</time>
 </template>
 
-<script lang="typescript">
-	import Vue from 'vue';
+<script lang="ts">
+import Vue from 'vue';
 
-	export default Vue.extend({
-		props: ['time', 'mode'],
-		data() {
-			return {
-				mode: 'relative',
-				tickId: null,
-				now: new Date()
-			};
+export default Vue.extend({
+	props: {
+		time: {
+			type: [Date, String],
+			required: true
 		},
-		computed: {
-			absolute() {
-				return (
-					this.time.getFullYear()    + '年' +
-					(this.time.getMonth() + 1) + '月' +
-					this.time.getDate()        + '日' +
-					' ' +
-					this.time.getHours()       + '時' +
-					this.time.getMinutes()     + '分');
-			},
-			relative() {
-				const ago = (this.now - this.time) / 1000/*ms*/;
-				return (
-					ago >= 31536000 ? '%i18n:common.time.years_ago%'  .replace('{}', ~~(ago / 31536000)) :
-					ago >= 2592000  ? '%i18n:common.time.months_ago%' .replace('{}', ~~(ago / 2592000)) :
-					ago >= 604800   ? '%i18n:common.time.weeks_ago%'  .replace('{}', ~~(ago / 604800)) :
-					ago >= 86400    ? '%i18n:common.time.days_ago%'   .replace('{}', ~~(ago / 86400)) :
-					ago >= 3600     ? '%i18n:common.time.hours_ago%'  .replace('{}', ~~(ago / 3600)) :
-					ago >= 60       ? '%i18n:common.time.minutes_ago%'.replace('{}', ~~(ago / 60)) :
-					ago >= 10       ? '%i18n:common.time.seconds_ago%'.replace('{}', ~~(ago % 60)) :
-					ago >= 0        ? '%i18n:common.time.just_now%' :
-					ago <  0        ? '%i18n:common.time.future%' :
-					'%i18n:common.time.unknown%');
-			}
-		},
-		created() {
-			if (this.mode == 'relative' || this.mode == 'detail') {
-				this.tick();
-				this.tickId = setInterval(this.tick, 1000);
-			}
-		},
-		destroyed() {
-			if (this.mode === 'relative' || this.mode === 'detail') {
-				clearInterval(this.tickId);
-			}
-		},
-		methods: {
-			tick() {
-				this.now = new Date();
-			}
+		mode: {
+			type: String,
+			default: 'relative'
 		}
-	});
+	},
+	data() {
+		return {
+			tickId: null,
+			now: new Date()
+		};
+	},
+	computed: {
+		_time(): Date {
+			return typeof this.time == 'string' ? new Date(this.time) : this.time;
+		},
+		absolute(): string {
+			const time = this._time;
+			return (
+				time.getFullYear()    + '年' +
+				(time.getMonth() + 1) + '月' +
+				time.getDate()        + '日' +
+				' ' +
+				time.getHours()       + '時' +
+				time.getMinutes()     + '分');
+		},
+		relative(): string {
+			const time = this._time;
+			const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/;
+			return (
+				ago >= 31536000 ? '%i18n:common.time.years_ago%'  .replace('{}', (~~(ago / 31536000)).toString()) :
+				ago >= 2592000  ? '%i18n:common.time.months_ago%' .replace('{}', (~~(ago / 2592000)).toString()) :
+				ago >= 604800   ? '%i18n:common.time.weeks_ago%'  .replace('{}', (~~(ago / 604800)).toString()) :
+				ago >= 86400    ? '%i18n:common.time.days_ago%'   .replace('{}', (~~(ago / 86400)).toString()) :
+				ago >= 3600     ? '%i18n:common.time.hours_ago%'  .replace('{}', (~~(ago / 3600)).toString()) :
+				ago >= 60       ? '%i18n:common.time.minutes_ago%'.replace('{}', (~~(ago / 60)).toString()) :
+				ago >= 10       ? '%i18n:common.time.seconds_ago%'.replace('{}', (~~(ago % 60)).toString()) :
+				ago >= 0        ? '%i18n:common.time.just_now%' :
+				ago <  0        ? '%i18n:common.time.future%' :
+				'%i18n:common.time.unknown%');
+		}
+	},
+	created() {
+		if (this.mode == 'relative' || this.mode == 'detail') {
+			this.tick();
+			this.tickId = setInterval(this.tick, 1000);
+		}
+	},
+	destroyed() {
+		if (this.mode === 'relative' || this.mode === 'detail') {
+			clearInterval(this.tickId);
+		}
+	},
+	methods: {
+		tick() {
+			this.now = new Date();
+		}
+	}
+});
 </script>
diff --git a/src/web/app/desktop/views/components/images-image.vue b/src/web/app/desktop/views/components/images-image.vue
index 8cb9d5e10..5ef8ffcda 100644
--- a/src/web/app/desktop/views/components/images-image.vue
+++ b/src/web/app/desktop/views/components/images-image.vue
@@ -4,7 +4,7 @@
 	@mousemove="onMousemove"
 	@mouseleave="onMouseleave"
 	@click.prevent="onClick"
-	:style="styles"
+	:style="style"
 	:title="image.name"></a>
 </template>
 
diff --git a/src/web/app/desktop/views/components/posts-post.vue b/src/web/app/desktop/views/components/posts-post.vue
index 2633a63f2..77a1e882c 100644
--- a/src/web/app/desktop/views/components/posts-post.vue
+++ b/src/web/app/desktop/views/components/posts-post.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-posts-post" tabindex="-1" :title="title" @keydown="onKeydown" @dblclick="onDblClick">
+<div class="mk-posts-post" tabindex="-1" :title="title" @keydown="onKeydown">
 	<div class="reply-to" v-if="p.reply">
 		<mk-posts-post-sub post="p.reply"/>
 	</div>
@@ -58,7 +58,7 @@
 				<button @click="menu" ref="menuButton">
 					%fa:ellipsis-h%
 				</button>
-				<button @click="toggleDetail" title="%i18n:desktop.tags.mk-timeline-post.detail">
+				<button title="%i18n:desktop.tags.mk-timeline-post.detail">
 					<template v-if="!isDetailOpened">%fa:caret-down%</template>
 					<template v-if="isDetailOpened">%fa:caret-up%</template>
 				</button>
@@ -94,6 +94,7 @@ export default Vue.extend({
 	props: ['post'],
 	data() {
 		return {
+			isDetailOpened: false,
 			connection: null,
 			connectionId: null
 		};
@@ -109,7 +110,11 @@ export default Vue.extend({
 			return this.isRepost ? this.post.repost : this.post;
 		},
 		reactionsCount(): number {
-			return this.p.reaction_counts ? Object.keys(this.p.reaction_counts).map(key => this.p.reaction_counts[key]).reduce((a, b) => a + b) : 0;
+			return this.p.reaction_counts
+				? Object.keys(this.p.reaction_counts)
+					.map(key => this.p.reaction_counts[key])
+					.reduce((a, b) => a + b)
+				: 0;
 		},
 		title(): string {
 			return dateStringify(this.p.created_at);

From 621744904793ff838b175fd94bdfddb4bc0b52e5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 14:05:41 +0900
Subject: [PATCH 077/286] wip

---
 src/web/app/common/mios.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index 550d9e6bf..a98df1bc0 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -115,6 +115,9 @@ export default class MiOS extends EventEmitter {
 			this.streams.driveStream = new DriveStreamManager(this.i);
 			this.streams.messagingIndexStream = new MessagingIndexStreamManager(this.i);
 		});
+
+		// TODO: this global export is for debugging. so disable this if production build
+		(window as any).os = this;
 	}
 
 	public log(...args) {

From cffda0bcd213b74da3e6463f50896b32aeba5211 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 15:17:59 +0900
Subject: [PATCH 078/286] wip

---
 src/web/app/common/-tags/messaging/room.tag   | 319 ------------------
 .../views/components/messaging-room.vue       | 314 +++++++++++++++++
 .../app/desktop/views/components/posts.vue    |   2 +-
 3 files changed, 315 insertions(+), 320 deletions(-)
 delete mode 100644 src/web/app/common/-tags/messaging/room.tag
 create mode 100644 src/web/app/common/views/components/messaging-room.vue

diff --git a/src/web/app/common/-tags/messaging/room.tag b/src/web/app/common/-tags/messaging/room.tag
deleted file mode 100644
index 990f20a8e..000000000
--- a/src/web/app/common/-tags/messaging/room.tag
+++ /dev/null
@@ -1,319 +0,0 @@
-<mk-messaging-room>
-	<div class="stream">
-		<p class="init" v-if="init">%fa:spinner .spin%%i18n:common.loading%</p>
-		<p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:common.tags.mk-messaging-room.empty%</p>
-		<p class="no-history" v-if="!init && messages.length > 0 && !moreMessagesIsInStock">%fa:flag%%i18n:common.tags.mk-messaging-room.no-history%</p>
-		<button class="more { fetching: fetchingMoreMessages }" v-if="moreMessagesIsInStock" @click="fetchMoreMessages" disabled={ fetchingMoreMessages }>
-			<template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' }
-		</button>
-		<template each={ message, i in messages }>
-			<mk-messaging-message message={ message }/>
-			<p class="date" v-if="i != messages.length - 1 && message._date != messages[i + 1]._date"><span>{ messages[i + 1]._datetext }</span></p>
-		</template>
-	</div>
-	<footer>
-		<div ref="notifications"></div>
-		<div class="grippie" title="%i18n:common.tags.mk-messaging-room.resize-form%"></div>
-		<mk-messaging-form user={ user }/>
-	</footer>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> .stream
-				max-width 600px
-				margin 0 auto
-
-				> .init
-					width 100%
-					margin 0
-					padding 16px 8px 8px 8px
-					text-align center
-					font-size 0.8em
-					color rgba(0, 0, 0, 0.4)
-
-					[data-fa]
-						margin-right 4px
-
-				> .empty
-					width 100%
-					margin 0
-					padding 16px 8px 8px 8px
-					text-align center
-					font-size 0.8em
-					color rgba(0, 0, 0, 0.4)
-
-					[data-fa]
-						margin-right 4px
-
-				> .no-history
-					display block
-					margin 0
-					padding 16px
-					text-align center
-					font-size 0.8em
-					color rgba(0, 0, 0, 0.4)
-
-					[data-fa]
-						margin-right 4px
-
-				> .more
-					display block
-					margin 16px auto
-					padding 0 12px
-					line-height 24px
-					color #fff
-					background rgba(0, 0, 0, 0.3)
-					border-radius 12px
-
-					&:hover
-						background rgba(0, 0, 0, 0.4)
-
-					&:active
-						background rgba(0, 0, 0, 0.5)
-
-					&.fetching
-						cursor wait
-
-					> [data-fa]
-						margin-right 4px
-
-				> .message
-					// something
-
-				> .date
-					display block
-					margin 8px 0
-					text-align center
-
-					&:before
-						content ''
-						display block
-						position absolute
-						height 1px
-						width 90%
-						top 16px
-						left 0
-						right 0
-						margin 0 auto
-						background rgba(0, 0, 0, 0.1)
-
-					> span
-						display inline-block
-						margin 0
-						padding 0 16px
-						//font-weight bold
-						line-height 32px
-						color rgba(0, 0, 0, 0.3)
-						background #fff
-
-			> footer
-				position -webkit-sticky
-				position sticky
-				z-index 2
-				bottom 0
-				width 100%
-				max-width 600px
-				margin 0 auto
-				padding 0
-				background rgba(255, 255, 255, 0.95)
-				background-clip content-box
-
-				> [ref='notifications']
-					position absolute
-					top -48px
-					width 100%
-					padding 8px 0
-					text-align center
-
-					&:empty
-						display none
-
-					> p
-						display inline-block
-						margin 0
-						padding 0 12px 0 28px
-						cursor pointer
-						line-height 32px
-						font-size 12px
-						color $theme-color-foreground
-						background $theme-color
-						border-radius 16px
-						transition opacity 1s ease
-
-						> [data-fa]
-							position absolute
-							top 0
-							left 10px
-							line-height 32px
-							font-size 16px
-
-				> .grippie
-					height 10px
-					margin-top -10px
-					background transparent
-					cursor ns-resize
-
-					&:hover
-						//background rgba(0, 0, 0, 0.1)
-
-					&:active
-						//background rgba(0, 0, 0, 0.2)
-
-	</style>
-	<script lang="typescript">
-		import MessagingStreamConnection from '../../scripts/streaming/messaging-stream';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.user = this.opts.user;
-		this.init = true;
-		this.sending = false;
-		this.messages = [];
-		this.isNaked = this.opts.isNaked;
-
-		this.connection = new MessagingStreamConnection(this.I, this.user.id);
-
-		this.on('mount', () => {
-			this.connection.on('message', this.onMessage);
-			this.connection.on('read', this.onRead);
-
-			document.addEventListener('visibilitychange', this.onVisibilitychange);
-
-			this.fetchMessages().then(() => {
-				this.init = false;
-				this.update();
-				this.scrollToBottom();
-			});
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('message', this.onMessage);
-			this.connection.off('read', this.onRead);
-			this.connection.close();
-
-			document.removeEventListener('visibilitychange', this.onVisibilitychange);
-		});
-
-		this.on('update', () => {
-			this.messages.forEach(message => {
-				const date = (new Date(message.created_at)).getDate();
-				const month = (new Date(message.created_at)).getMonth() + 1;
-				message._date = date;
-				message._datetext = month + '月 ' + date + '日';
-			});
-		});
-
-		this.onMessage = (message) => {
-			const isBottom = this.isBottom();
-
-			this.messages.push(message);
-			if (message.user_id != this.I.id && !document.hidden) {
-				this.connection.send({
-					type: 'read',
-					id: message.id
-				});
-			}
-			this.update();
-
-			if (isBottom) {
-				// Scroll to bottom
-				this.scrollToBottom();
-			} else if (message.user_id != this.I.id) {
-				// Notify
-				this.notify('%i18n:common.tags.mk-messaging-room.new-message%');
-			}
-		};
-
-		this.onRead = ids => {
-			if (!Array.isArray(ids)) ids = [ids];
-			ids.forEach(id => {
-				if (this.messages.some(x => x.id == id)) {
-					const exist = this.messages.map(x => x.id).indexOf(id);
-					this.messages[exist].is_read = true;
-					this.update();
-				}
-			});
-		};
-
-		this.fetchMoreMessages = () => {
-			this.update({
-				fetchingMoreMessages: true
-			});
-			this.fetchMessages().then(() => {
-				this.update({
-					fetchingMoreMessages: false
-				});
-			});
-		};
-
-		this.fetchMessages = () => new Promise((resolve, reject) => {
-			const max = this.moreMessagesIsInStock ? 20 : 10;
-
-			this.api('messaging/messages', {
-				user_id: this.user.id,
-				limit: max + 1,
-				until_id: this.moreMessagesIsInStock ? this.messages[0].id : undefined
-			}).then(messages => {
-				if (messages.length == max + 1) {
-					this.moreMessagesIsInStock = true;
-					messages.pop();
-				} else {
-					this.moreMessagesIsInStock = false;
-				}
-
-				this.messages.unshift.apply(this.messages, messages.reverse());
-				this.update();
-
-				resolve();
-			});
-		});
-
-		this.isBottom = () => {
-			const asobi = 32;
-			const current = this.isNaked
-				? window.scrollY + window.innerHeight
-				: this.root.scrollTop + this.root.offsetHeight;
-			const max = this.isNaked
-				? document.body.offsetHeight
-				: this.root.scrollHeight;
-			return current > (max - asobi);
-		};
-
-		this.scrollToBottom = () => {
-			if (this.isNaked) {
-				window.scroll(0, document.body.offsetHeight);
-			} else {
-				this.root.scrollTop = this.root.scrollHeight;
-			}
-		};
-
-		this.notify = message => {
-			const n = document.createElement('p');
-			n.innerHTML = '%fa:arrow-circle-down%' + message;
-			n.onclick = () => {
-				this.scrollToBottom();
-				n.parentNode.removeChild(n);
-			};
-			this.$refs.notifications.appendChild(n);
-
-			setTimeout(() => {
-				n.style.opacity = 0;
-				setTimeout(() => n.parentNode.removeChild(n), 1000);
-			}, 4000);
-		};
-
-		this.onVisibilitychange = () => {
-			if (document.hidden) return;
-			this.messages.forEach(message => {
-				if (message.user_id !== this.I.id && !message.is_read) {
-					this.connection.send({
-						type: 'read',
-						id: message.id
-					});
-				}
-			});
-		};
-	</script>
-</mk-messaging-room>
diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
new file mode 100644
index 000000000..2fb6671b8
--- /dev/null
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -0,0 +1,314 @@
+<template>
+<div class="mk-messaging-room">
+	<div class="stream">
+		<p class="init" v-if="init">%fa:spinner .spin%%i18n:common.loading%</p>
+		<p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:common.tags.mk-messaging-room.empty%</p>
+		<p class="no-history" v-if="!init && messages.length > 0 && !moreMessagesIsInStock">%fa:flag%%i18n:common.tags.mk-messaging-room.no-history%</p>
+		<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="moreMessagesIsInStock" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
+			<template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' }}
+		</button>
+		<template v-for="(message, i) in messages">
+			<mk-messaging-message :message="message" :key="message.id"/>
+			<p class="date" :key="message.id + '-time'" v-if="i != messages.length - 1 && _message._date != _messages[i + 1]._date"><span>{{ _messages[i + 1]._datetext }}</span></p>
+		</template>
+	</div>
+	<footer>
+		<div ref="notifications"></div>
+		<div class="grippie" title="%i18n:common.tags.mk-messaging-room.resize-form%"></div>
+		<mk-messaging-form :user="user"/>
+	</footer>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import MessagingStreamConnection from '../../scripts/streaming/messaging-stream';
+
+export default Vue.extend({
+	props: ['user', 'isNaked'],
+	data() {
+		return {
+			init: true,
+			fetchingMoreMessages: false,
+			messages: [],
+			existMoreMessages: false,
+			connection: null
+		};
+	},
+	computed: {
+		_messages(): any[] {
+			return (this.messages as any).map(message => {
+				const date = new Date(message.created_at).getDate();
+				const month = new Date(message.created_at).getMonth() + 1;
+				message._date = date;
+				message._datetext = `${month}月 ${date}日`;
+				return message;
+			});
+		}
+	},
+
+	mounted() {
+		this.connection = new MessagingStreamConnection(this.$root.$data.os.i, this.user.id);
+
+		this.connection.on('message', this.onMessage);
+		this.connection.on('read', this.onRead);
+
+		document.addEventListener('visibilitychange', this.onVisibilitychange);
+
+		this.fetchMessages().then(() => {
+			this.init = false;
+			this.scrollToBottom();
+		});
+	},
+	beforeDestroy() {
+		this.connection.off('message', this.onMessage);
+		this.connection.off('read', this.onRead);
+		this.connection.close();
+
+		document.removeEventListener('visibilitychange', this.onVisibilitychange);
+	},
+	methods: {
+		fetchMessages() {
+			return new Promise((resolve, reject) => {
+				const max = this.existMoreMessages ? 20 : 10;
+
+				this.$root.$data.os.api('messaging/messages', {
+					user_id: this.user.id,
+					limit: max + 1,
+					until_id: this.existMoreMessages ? this.messages[0].id : undefined
+				}).then(messages => {
+					if (messages.length == max + 1) {
+						this.existMoreMessages = true;
+						messages.pop();
+					} else {
+						this.existMoreMessages = false;
+					}
+
+					this.messages.unshift.apply(this.messages, messages.reverse());
+					resolve();
+				});
+			});
+		},
+		fetchMoreMessages() {
+			this.fetchingMoreMessages = true;
+			this.fetchMessages().then(() => {
+				this.fetchingMoreMessages = false;
+			});
+		},
+		onMessage(message) {
+			const isBottom = this.isBottom();
+
+			this.messages.push(message);
+			if (message.user_id != this.$root.$data.os.i.id && !document.hidden) {
+				this.connection.send({
+					type: 'read',
+					id: message.id
+				});
+			}
+
+			if (isBottom) {
+				// Scroll to bottom
+				this.scrollToBottom();
+			} else if (message.user_id != this.$root.$data.os.i.id) {
+				// Notify
+				this.notify('%i18n:common.tags.mk-messaging-room.new-message%');
+			}
+		},
+		onRead(ids) {
+			if (!Array.isArray(ids)) ids = [ids];
+			ids.forEach(id => {
+				if (this.messages.some(x => x.id == id)) {
+					const exist = this.messages.map(x => x.id).indexOf(id);
+					this.messages[exist].is_read = true;
+				}
+			});
+		},
+		isBottom() {
+			const asobi = 32;
+			const current = this.isNaked
+				? window.scrollY + window.innerHeight
+				: this.$el.scrollTop + this.$el.offsetHeight;
+			const max = this.isNaked
+				? document.body.offsetHeight
+				: this.$el.scrollHeight;
+			return current > (max - asobi);
+		},
+		scrollToBottom() {
+			if (this.isNaked) {
+				window.scroll(0, document.body.offsetHeight);
+			} else {
+				this.$el.scrollTop = this.$el.scrollHeight;
+			}
+		},
+		notify(message) {
+			const n = document.createElement('p') as any;
+			n.innerHTML = '%fa:arrow-circle-down%' + message;
+			n.onclick = () => {
+				this.scrollToBottom();
+				n.parentNode.removeChild(n);
+			};
+			(this.$refs.notifications as any).appendChild(n);
+
+			setTimeout(() => {
+				n.style.opacity = 0;
+				setTimeout(() => n.parentNode.removeChild(n), 1000);
+			}, 4000);
+		},
+		onVisibilitychange() {
+			if (document.hidden) return;
+			this.messages.forEach(message => {
+				if (message.user_id !== this.$root.$data.os.i.id && !message.is_read) {
+					this.connection.send({
+						type: 'read',
+						id: message.id
+					});
+				}
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-messaging-room
+	> .stream
+		max-width 600px
+		margin 0 auto
+
+		> .init
+			width 100%
+			margin 0
+			padding 16px 8px 8px 8px
+			text-align center
+			font-size 0.8em
+			color rgba(0, 0, 0, 0.4)
+
+			[data-fa]
+				margin-right 4px
+
+		> .empty
+			width 100%
+			margin 0
+			padding 16px 8px 8px 8px
+			text-align center
+			font-size 0.8em
+			color rgba(0, 0, 0, 0.4)
+
+			[data-fa]
+				margin-right 4px
+
+		> .no-history
+			display block
+			margin 0
+			padding 16px
+			text-align center
+			font-size 0.8em
+			color rgba(0, 0, 0, 0.4)
+
+			[data-fa]
+				margin-right 4px
+
+		> .more
+			display block
+			margin 16px auto
+			padding 0 12px
+			line-height 24px
+			color #fff
+			background rgba(0, 0, 0, 0.3)
+			border-radius 12px
+
+			&:hover
+				background rgba(0, 0, 0, 0.4)
+
+			&:active
+				background rgba(0, 0, 0, 0.5)
+
+			&.fetching
+				cursor wait
+
+			> [data-fa]
+				margin-right 4px
+
+		> .message
+			// something
+
+		> .date
+			display block
+			margin 8px 0
+			text-align center
+
+			&:before
+				content ''
+				display block
+				position absolute
+				height 1px
+				width 90%
+				top 16px
+				left 0
+				right 0
+				margin 0 auto
+				background rgba(0, 0, 0, 0.1)
+
+			> span
+				display inline-block
+				margin 0
+				padding 0 16px
+				//font-weight bold
+				line-height 32px
+				color rgba(0, 0, 0, 0.3)
+				background #fff
+
+	> footer
+		position -webkit-sticky
+		position sticky
+		z-index 2
+		bottom 0
+		width 100%
+		max-width 600px
+		margin 0 auto
+		padding 0
+		background rgba(255, 255, 255, 0.95)
+		background-clip content-box
+
+		> [ref='notifications']
+			position absolute
+			top -48px
+			width 100%
+			padding 8px 0
+			text-align center
+
+			&:empty
+				display none
+
+			> p
+				display inline-block
+				margin 0
+				padding 0 12px 0 28px
+				cursor pointer
+				line-height 32px
+				font-size 12px
+				color $theme-color-foreground
+				background $theme-color
+				border-radius 16px
+				transition opacity 1s ease
+
+				> [data-fa]
+					position absolute
+					top 0
+					left 10px
+					line-height 32px
+					font-size 16px
+
+		> .grippie
+			height 10px
+			margin-top -10px
+			background transparent
+			cursor ns-resize
+
+			&:hover
+				//background rgba(0, 0, 0, 0.1)
+
+			&:active
+				//background rgba(0, 0, 0, 0.2)
+
+</style>
diff --git a/src/web/app/desktop/views/components/posts.vue b/src/web/app/desktop/views/components/posts.vue
index 880ee5224..6c73731bf 100644
--- a/src/web/app/desktop/views/components/posts.vue
+++ b/src/web/app/desktop/views/components/posts.vue
@@ -2,7 +2,7 @@
 <div class="mk-posts">
 	<template v-for="(post, i) in _posts">
 		<mk-posts-post :post.sync="post" :key="post.id"/>
-		<p class="date" :key="post.id + '-time'" v-if="i != _posts.length - 1 && post._date != _posts[i + 1]._date"><span>%fa:angle-up%{{ post._datetext }}</span><span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span></p>
+		<p class="date" :key="post.id + '-time'" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date"><span>%fa:angle-up%{{ post._datetext }}</span><span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span></p>
 	</template>
 	<footer>
 		<slot name="footer"></slot>

From d03594911ed774ed2166aadf8e9370c0f84cbb88 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 15:19:43 +0900
Subject: [PATCH 079/286] wip

---
 src/web/app/common/-tags/index.ts | 30 ------------------------------
 src/web/app/common/-tags/raw.tag  | 13 -------------
 2 files changed, 43 deletions(-)
 delete mode 100644 src/web/app/common/-tags/index.ts
 delete mode 100644 src/web/app/common/-tags/raw.tag

diff --git a/src/web/app/common/-tags/index.ts b/src/web/app/common/-tags/index.ts
deleted file mode 100644
index df99d93cc..000000000
--- a/src/web/app/common/-tags/index.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-require('./error.tag');
-require('./url.tag');
-require('./url-preview.tag');
-require('./time.tag');
-require('./file-type-icon.tag');
-require('./uploader.tag');
-require('./ellipsis.tag');
-require('./raw.tag');
-require('./number.tag');
-require('./special-message.tag');
-require('./signin.tag');
-require('./signup.tag');
-require('./forkit.tag');
-require('./introduction.tag');
-require('./signin-history.tag');
-require('./twitter-setting.tag');
-require('./authorized-apps.tag');
-require('./poll.tag');
-require('./poll-editor.tag');
-require('./messaging/room.tag');
-require('./messaging/message.tag');
-require('./messaging/index.tag');
-require('./messaging/form.tag');
-require('./stream-indicator.tag');
-require('./activity-table.tag');
-require('./reaction-picker.tag');
-require('./reactions-viewer.tag');
-require('./reaction-icon.tag');
-require('./post-menu.tag');
-require('./nav-links.tag');
diff --git a/src/web/app/common/-tags/raw.tag b/src/web/app/common/-tags/raw.tag
deleted file mode 100644
index 149ac6c4b..000000000
--- a/src/web/app/common/-tags/raw.tag
+++ /dev/null
@@ -1,13 +0,0 @@
-<mk-raw>
-	<style lang="stylus" scoped>
-		:scope
-			display inline
-	</style>
-	<script lang="typescript">
-		this.root.innerHTML = this.opts.content;
-
-		this.on('updated', () => {
-			this.root.innerHTML = this.opts.content;
-		});
-	</script>
-</mk-raw>

From 88ded14518af6df9417ca983dd0ca25792d0c77c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 15:38:53 +0900
Subject: [PATCH 080/286] wip

---
 .../app/common/-tags/messaging/message.tag    | 238 ------------------
 .../views/components/messaging-message.vue    | 233 +++++++++++++++++
 2 files changed, 233 insertions(+), 238 deletions(-)
 delete mode 100644 src/web/app/common/-tags/messaging/message.tag
 create mode 100644 src/web/app/common/views/components/messaging-message.vue

diff --git a/src/web/app/common/-tags/messaging/message.tag b/src/web/app/common/-tags/messaging/message.tag
deleted file mode 100644
index ba6d26a18..000000000
--- a/src/web/app/common/-tags/messaging/message.tag
+++ /dev/null
@@ -1,238 +0,0 @@
-<mk-messaging-message data-is-me={ message.is_me }>
-	<a class="avatar-anchor" href={ '/' + message.user.username } title={ message.user.username } target="_blank">
-		<img class="avatar" src={ message.user.avatar_url + '?thumbnail&size=80' } alt=""/>
-	</a>
-	<div class="content-container">
-		<div class="balloon">
-			<p class="read" v-if="message.is_me && message.is_read">%i18n:common.tags.mk-messaging-message.is-read%</p>
-			<button class="delete-button" v-if="message.is_me" title="%i18n:common.delete%"><img src="/assets/desktop/messaging/delete.png" alt="Delete"/></button>
-			<div class="content" v-if="!message.is_deleted">
-				<div ref="text"></div>
-				<div class="image" v-if="message.file"><img src={ message.file.url } alt="image" title={ message.file.name }/></div>
-			</div>
-			<div class="content" v-if="message.is_deleted">
-				<p class="is-deleted">%i18n:common.tags.mk-messaging-message.deleted%</p>
-			</div>
-		</div>
-		<footer>
-			<mk-time time={ message.created_at }/><template v-if="message.is_edited">%fa:pencil-alt%</template>
-		</footer>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			$me-balloon-color = #23A7B6
-
-			display block
-			padding 10px 12px 10px 12px
-			background-color transparent
-
-			&:after
-				content ""
-				display block
-				clear both
-
-			> .avatar-anchor
-				display block
-
-				> .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
-
-			> .content-container
-				display block
-				margin 0 12px
-				padding 0
-				max-width calc(100% - 78px)
-
-				> .balloon
-					display block
-					float inherit
-					margin 0
-					padding 0
-					max-width 100%
-					min-height 38px
-					border-radius 16px
-
-					&:before
-						content ""
-						pointer-events none
-						display block
-						position absolute
-						top 12px
-
-					&:hover
-						> .delete-button
-							display block
-
-					> .delete-button
-						display none
-						position absolute
-						z-index 1
-						top -4px
-						right -4px
-						margin 0
-						padding 0
-						cursor pointer
-						outline none
-						border none
-						border-radius 0
-						box-shadow none
-						background transparent
-
-						> img
-							vertical-align bottom
-							width 16px
-							height 16px
-							cursor pointer
-
-					> .read
-						user-select none
-						display block
-						position absolute
-						z-index 1
-						bottom -4px
-						left -12px
-						margin 0
-						color rgba(0, 0, 0, 0.5)
-						font-size 11px
-
-					> .content
-
-						> .is-deleted
-							display block
-							margin 0
-							padding 0
-							overflow hidden
-							overflow-wrap break-word
-							font-size 1em
-							color rgba(0, 0, 0, 0.5)
-
-						> [ref='text']
-							display block
-							margin 0
-							padding 8px 16px
-							overflow hidden
-							overflow-wrap break-word
-							font-size 1em
-							color rgba(0, 0, 0, 0.8)
-
-							&, *
-								user-select text
-								cursor auto
-
-							& + .file
-								&.image
-									> img
-										border-radius 0 0 16px 16px
-
-						> .file
-							&.image
-								> img
-									display block
-									max-width 100%
-									max-height 512px
-									border-radius 16px
-
-				> footer
-					display block
-					clear both
-					margin 0
-					padding 2px
-					font-size 10px
-					color rgba(0, 0, 0, 0.4)
-
-					> [data-fa]
-						margin-left 4px
-
-			&:not([data-is-me='true'])
-				> .avatar-anchor
-					float left
-
-				> .content-container
-					float left
-
-					> .balloon
-						background #eee
-
-						&:before
-							left -14px
-							border-top solid 8px transparent
-							border-right solid 8px #eee
-							border-bottom solid 8px transparent
-							border-left solid 8px transparent
-
-					> footer
-						text-align left
-
-			&[data-is-me='true']
-				> .avatar-anchor
-					float right
-
-				> .content-container
-					float right
-
-					> .balloon
-						background $me-balloon-color
-
-						&:before
-							right -14px
-							left auto
-							border-top solid 8px transparent
-							border-right solid 8px transparent
-							border-bottom solid 8px transparent
-							border-left solid 8px $me-balloon-color
-
-						> .content
-
-							> p.is-deleted
-								color rgba(255, 255, 255, 0.5)
-
-							> [ref='text']
-								&, *
-									color #fff !important
-
-					> footer
-						text-align right
-
-			&[data-is-deleted='true']
-					> .content-container
-						opacity 0.5
-
-	</style>
-	<script lang="typescript">
-		import compile from '../../../common/scripts/text-compiler';
-
-		this.mixin('i');
-
-		this.message = this.opts.message;
-		this.message.is_me = this.message.user.id == this.I.id;
-
-		this.on('mount', () => {
-			if (this.message.text) {
-				const tokens = this.message.ast;
-
-				this.$refs.text.innerHTML = compile(tokens);
-
-				Array.from(this.$refs.text.children).forEach(e => {
-					if (e.tagName == 'MK-URL') riot.mount(e);
-				});
-
-				// URLをプレビュー
-				tokens
-					.filter(t => t.type == 'link')
-					.map(t => {
-						const el = this.$refs.text.appendChild(document.createElement('mk-url-preview'));
-						riot.mount(el, {
-							url: t.content
-						});
-					});
-			}
-		});
-	</script>
-</mk-messaging-message>
diff --git a/src/web/app/common/views/components/messaging-message.vue b/src/web/app/common/views/components/messaging-message.vue
new file mode 100644
index 000000000..b1afe7a69
--- /dev/null
+++ b/src/web/app/common/views/components/messaging-message.vue
@@ -0,0 +1,233 @@
+<template>
+<div class="mk-messaging-message" :data-is-me="isMe">
+	<a class="avatar-anchor" href={ '/' + message.user.username } title={ message.user.username } target="_blank">
+		<img class="avatar" src={ message.user.avatar_url + '?thumbnail&size=80' } alt=""/>
+	</a>
+	<div class="content-container">
+		<div class="balloon">
+			<p class="read" v-if="message.is_me && message.is_read">%i18n:common.tags.mk-messaging-message.is-read%</p>
+			<button class="delete-button" v-if="message.is_me" title="%i18n:common.delete%"><img src="/assets/desktop/messaging/delete.png" alt="Delete"/></button>
+			<div class="content" v-if="!message.is_deleted">
+				<mk-post-html v-if="message.ast" :ast="message.ast" :i="$root.$data.os.i"/>
+				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+				<div class="image" v-if="message.file"><img src={ message.file.url } alt="image" title={ message.file.name }/></div>
+			</div>
+			<div class="content" v-if="message.is_deleted">
+				<p class="is-deleted">%i18n:common.tags.mk-messaging-message.deleted%</p>
+			</div>
+		</div>
+		<footer>
+			<mk-time time={ message.created_at }/><template v-if="message.is_edited">%fa:pencil-alt%</template>
+		</footer>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['message'],
+	computed: {
+		isMe(): boolean {
+			return this.message.user_id == this.$root.$data.os.i.id;
+		},
+		urls(): string[] {
+			if (this.message.ast) {
+				return this.message.ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-messaging-message
+	$me-balloon-color = #23A7B6
+
+	padding 10px 12px 10px 12px
+	background-color transparent
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	> .avatar-anchor
+		display block
+
+		> .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
+
+	> .content-container
+		display block
+		margin 0 12px
+		padding 0
+		max-width calc(100% - 78px)
+
+		> .balloon
+			display block
+			float inherit
+			margin 0
+			padding 0
+			max-width 100%
+			min-height 38px
+			border-radius 16px
+
+			&:before
+				content ""
+				pointer-events none
+				display block
+				position absolute
+				top 12px
+
+			&:hover
+				> .delete-button
+					display block
+
+			> .delete-button
+				display none
+				position absolute
+				z-index 1
+				top -4px
+				right -4px
+				margin 0
+				padding 0
+				cursor pointer
+				outline none
+				border none
+				border-radius 0
+				box-shadow none
+				background transparent
+
+				> img
+					vertical-align bottom
+					width 16px
+					height 16px
+					cursor pointer
+
+			> .read
+				user-select none
+				display block
+				position absolute
+				z-index 1
+				bottom -4px
+				left -12px
+				margin 0
+				color rgba(0, 0, 0, 0.5)
+				font-size 11px
+
+			> .content
+
+				> .is-deleted
+					display block
+					margin 0
+					padding 0
+					overflow hidden
+					overflow-wrap break-word
+					font-size 1em
+					color rgba(0, 0, 0, 0.5)
+
+				> [ref='text']
+					display block
+					margin 0
+					padding 8px 16px
+					overflow hidden
+					overflow-wrap break-word
+					font-size 1em
+					color rgba(0, 0, 0, 0.8)
+
+					&, *
+						user-select text
+						cursor auto
+
+					& + .file
+						&.image
+							> img
+								border-radius 0 0 16px 16px
+
+				> .file
+					&.image
+						> img
+							display block
+							max-width 100%
+							max-height 512px
+							border-radius 16px
+
+		> footer
+			display block
+			clear both
+			margin 0
+			padding 2px
+			font-size 10px
+			color rgba(0, 0, 0, 0.4)
+
+			> [data-fa]
+				margin-left 4px
+
+	&:not([data-is-me='true'])
+		> .avatar-anchor
+			float left
+
+		> .content-container
+			float left
+
+			> .balloon
+				background #eee
+
+				&:before
+					left -14px
+					border-top solid 8px transparent
+					border-right solid 8px #eee
+					border-bottom solid 8px transparent
+					border-left solid 8px transparent
+
+			> footer
+				text-align left
+
+	&[data-is-me='true']
+		> .avatar-anchor
+			float right
+
+		> .content-container
+			float right
+
+			> .balloon
+				background $me-balloon-color
+
+				&:before
+					right -14px
+					left auto
+					border-top solid 8px transparent
+					border-right solid 8px transparent
+					border-bottom solid 8px transparent
+					border-left solid 8px $me-balloon-color
+
+				> .content
+
+					> p.is-deleted
+						color rgba(255, 255, 255, 0.5)
+
+					> [ref='text']
+						&, *
+							color #fff !important
+
+			> footer
+				text-align right
+
+	&[data-is-deleted='true']
+			> .content-container
+				opacity 0.5
+
+</style>

From e676bb159585a0d6f15b3503fad7510553976e36 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 15:56:11 +0900
Subject: [PATCH 081/286] wip

---
 src/web/app/common/-tags/messaging/form.tag   | 175 ----------------
 .../views/components/messaging-form.vue       | 186 ++++++++++++++++++
 2 files changed, 186 insertions(+), 175 deletions(-)
 delete mode 100644 src/web/app/common/-tags/messaging/form.tag
 create mode 100644 src/web/app/common/views/components/messaging-form.vue

diff --git a/src/web/app/common/-tags/messaging/form.tag b/src/web/app/common/-tags/messaging/form.tag
deleted file mode 100644
index 9a58dc0ce..000000000
--- a/src/web/app/common/-tags/messaging/form.tag
+++ /dev/null
@@ -1,175 +0,0 @@
-<mk-messaging-form>
-	<textarea ref="text" onkeypress={ onkeypress } onpaste={ onpaste } placeholder="%i18n:common.input-message-here%"></textarea>
-	<div class="files"></div>
-	<mk-uploader ref="uploader"/>
-	<button class="send" @click="send" disabled={ sending } title="%i18n:common.send%">
-		<template v-if="!sending">%fa:paper-plane%</template><template v-if="sending">%fa:spinner .spin%</template>
-	</button>
-	<button class="attach-from-local" type="button" title="%i18n:common.tags.mk-messaging-form.attach-from-local%">
-		%fa:upload%
-	</button>
-	<button class="attach-from-drive" type="button" title="%i18n:common.tags.mk-messaging-form.attach-from-drive%">
-		%fa:R folder-open%
-	</button>
-	<input name="file" type="file" accept="image/*"/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> textarea
-				cursor auto
-				display block
-				width 100%
-				min-width 100%
-				max-width 100%
-				height 64px
-				margin 0
-				padding 8px
-				font-size 1em
-				color #000
-				outline none
-				border none
-				border-top solid 1px #eee
-				border-radius 0
-				box-shadow none
-				background transparent
-
-			> .send
-				position absolute
-				bottom 0
-				right 0
-				margin 0
-				padding 10px 14px
-				line-height 1em
-				font-size 1em
-				color #aaa
-				transition color 0.1s ease
-
-				&:hover
-					color $theme-color
-
-				&:active
-					color darken($theme-color, 10%)
-					transition color 0s ease
-
-			.files
-				display block
-				margin 0
-				padding 0 8px
-				list-style none
-
-				&:after
-					content ''
-					display block
-					clear both
-
-				> li
-					display block
-					float left
-					margin 4px
-					padding 0
-					width 64px
-					height 64px
-					background-color #eee
-					background-repeat no-repeat
-					background-position center center
-					background-size cover
-					cursor move
-
-					&:hover
-						> .remove
-							display block
-
-					> .remove
-						display none
-						position absolute
-						right -6px
-						top -6px
-						margin 0
-						padding 0
-						background transparent
-						outline none
-						border none
-						border-radius 0
-						box-shadow none
-						cursor pointer
-
-			.attach-from-local
-			.attach-from-drive
-				margin 0
-				padding 10px 14px
-				line-height 1em
-				font-size 1em
-				font-weight normal
-				text-decoration none
-				color #aaa
-				transition color 0.1s ease
-
-				&:hover
-					color $theme-color
-
-				&:active
-					color darken($theme-color, 10%)
-					transition color 0s ease
-
-			input[type=file]
-				display none
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.onpaste = e => {
-			const data = e.clipboardData;
-			const items = data.items;
-			for (const item of items) {
-				if (item.kind == 'file') {
-					this.upload(item.getAsFile());
-				}
-			}
-		};
-
-		this.onkeypress = e => {
-			if ((e.which == 10 || e.which == 13) && e.ctrlKey) {
-				this.send();
-			}
-		};
-
-		this.selectFile = () => {
-			this.$refs.file.click();
-		};
-
-		this.selectFileFromDrive = () => {
-			const browser = document.body.appendChild(document.createElement('mk-select-file-from-drive-window'));
-			const event = riot.observable();
-			riot.mount(browser, {
-				multiple: true,
-				event: event
-			});
-			event.one('selected', files => {
-				files.forEach(this.addFile);
-			});
-		};
-
-		this.send = () => {
-			this.sending = true;
-			this.api('messaging/messages/create', {
-				user_id: this.opts.user.id,
-				text: this.$refs.text.value
-			}).then(message => {
-				this.clear();
-			}).catch(err => {
-				console.error(err);
-			}).then(() => {
-				this.sending = false;
-				this.update();
-			});
-		};
-
-		this.clear = () => {
-			this.$refs.text.value = '';
-			this.files = [];
-			this.update();
-		};
-	</script>
-</mk-messaging-form>
diff --git a/src/web/app/common/views/components/messaging-form.vue b/src/web/app/common/views/components/messaging-form.vue
new file mode 100644
index 000000000..bf4dd17ba
--- /dev/null
+++ b/src/web/app/common/views/components/messaging-form.vue
@@ -0,0 +1,186 @@
+<template>
+<div>
+	<textarea v-model="text" @keypress="onKeypress" @paste="onPaste" placeholder="%i18n:common.input-message-here%"></textarea>
+	<div class="files"></div>
+	<mk-uploader ref="uploader"/>
+	<button class="send" @click="send" :disabled="sending" title="%i18n:common.send%">
+		<template v-if="!sending">%fa:paper-plane%</template><template v-if="sending">%fa:spinner .spin%</template>
+	</button>
+	<button class="attach-from-local" type="button" title="%i18n:common.tags.mk-messaging-form.attach-from-local%">
+		%fa:upload%
+	</button>
+	<button class="attach-from-drive" type="button" title="%i18n:common.tags.mk-messaging-form.attach-from-drive%">
+		%fa:R folder-open%
+	</button>
+	<input name="file" type="file" accept="image/*"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	data() {
+		return {
+			text: null,
+			files: [],
+			sending: false
+		};
+	},
+	methods: {
+		onPaste(e) {
+			const data = e.clipboardData;
+			const items = data.items;
+			for (const item of items) {
+				if (item.kind == 'file') {
+					this.upload(item.getAsFile());
+				}
+			}
+		},
+
+		onKeypress(e) {
+			if ((e.which == 10 || e.which == 13) && e.ctrlKey) {
+				this.send();
+			}
+		},
+
+		chooseFile() {
+			(this.$refs.file as any).click();
+		},
+
+		chooseFileFromDrive() {
+			const w = new MkDriveChooserWindow({
+				propsData: {
+					multiple: true
+				}
+			}).$mount();
+			w.$once('selected', files => {
+				files.forEach(this.addFile);
+			});
+			document.body.appendChild(w.$el);
+		},
+
+		send() {
+			this.sending = true;
+			this.$root.$data.os.api('messaging/messages/create', {
+				user_id: this.user.id,
+				text: this.text
+			}).then(message => {
+				this.clear();
+			}).catch(err => {
+				console.error(err);
+			}).then(() => {
+				this.sending = false;
+			});
+		},
+
+		clear() {
+			this.text = '';
+			this.files = [];
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-messaging-form
+	> textarea
+		cursor auto
+		display block
+		width 100%
+		min-width 100%
+		max-width 100%
+		height 64px
+		margin 0
+		padding 8px
+		font-size 1em
+		color #000
+		outline none
+		border none
+		border-top solid 1px #eee
+		border-radius 0
+		box-shadow none
+		background transparent
+
+	> .send
+		position absolute
+		bottom 0
+		right 0
+		margin 0
+		padding 10px 14px
+		line-height 1em
+		font-size 1em
+		color #aaa
+		transition color 0.1s ease
+
+		&:hover
+			color $theme-color
+
+		&:active
+			color darken($theme-color, 10%)
+			transition color 0s ease
+
+	.files
+		display block
+		margin 0
+		padding 0 8px
+		list-style none
+
+		&:after
+			content ''
+			display block
+			clear both
+
+		> li
+			display block
+			float left
+			margin 4px
+			padding 0
+			width 64px
+			height 64px
+			background-color #eee
+			background-repeat no-repeat
+			background-position center center
+			background-size cover
+			cursor move
+
+			&:hover
+				> .remove
+					display block
+
+			> .remove
+				display none
+				position absolute
+				right -6px
+				top -6px
+				margin 0
+				padding 0
+				background transparent
+				outline none
+				border none
+				border-radius 0
+				box-shadow none
+				cursor pointer
+
+	.attach-from-local
+	.attach-from-drive
+		margin 0
+		padding 10px 14px
+		line-height 1em
+		font-size 1em
+		font-weight normal
+		text-decoration none
+		color #aaa
+		transition color 0.1s ease
+
+		&:hover
+			color $theme-color
+
+		&:active
+			color darken($theme-color, 10%)
+			transition color 0s ease
+
+	input[type=file]
+		display none
+
+</style>

From 0fbc228c35a5aef8f6e8fb008e684c5b767ce94d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 16:00:08 +0900
Subject: [PATCH 082/286] wip

---
 src/web/app/common/-tags/number.tag | 16 ----------------
 1 file changed, 16 deletions(-)
 delete mode 100644 src/web/app/common/-tags/number.tag

diff --git a/src/web/app/common/-tags/number.tag b/src/web/app/common/-tags/number.tag
deleted file mode 100644
index 9cbbacd2c..000000000
--- a/src/web/app/common/-tags/number.tag
+++ /dev/null
@@ -1,16 +0,0 @@
-<mk-number>
-	<style lang="stylus" scoped>
-		:scope
-			display inline
-	</style>
-	<script lang="typescript">
-		this.on('mount', () => {
-			let value = this.opts.value;
-			const max = this.opts.max;
-
-			if (max != null && value > max) value = max;
-
-			this.root.innerHTML = value.toLocaleString();
-		});
-	</script>
-</mk-number>

From 4d222f464b7c5d41aecf74545ed95e827a237958 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 16:24:21 +0900
Subject: [PATCH 083/286] wip

---
 src/web/app/desktop/-tags/follow-button.tag   | 150 ------------------
 .../views/components/follow-button.vue        | 149 +++++++++++++++++
 2 files changed, 149 insertions(+), 150 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/follow-button.tag
 create mode 100644 src/web/app/desktop/views/components/follow-button.vue

diff --git a/src/web/app/desktop/-tags/follow-button.tag b/src/web/app/desktop/-tags/follow-button.tag
deleted file mode 100644
index fa7d43e03..000000000
--- a/src/web/app/desktop/-tags/follow-button.tag
+++ /dev/null
@@ -1,150 +0,0 @@
-<mk-follow-button>
-	<button :class="{ wait: wait, follow: !user.is_following, unfollow: user.is_following }" v-if="!init" @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
-		<template v-if="!wait && user.is_following">%fa:minus%</template>
-		<template v-if="!wait && !user.is_following">%fa:plus%</template>
-		<template v-if="wait">%fa:spinner .pulse .fw%</template>
-	</button>
-	<div class="init" v-if="init">%fa:spinner .pulse .fw%</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> button
-			> .init
-				display block
-				cursor pointer
-				padding 0
-				margin 0
-				width 32px
-				height 32px
-				font-size 1em
-				outline none
-				border-radius 4px
-
-				*
-					pointer-events none
-
-				&:focus
-					&:after
-						content ""
-						pointer-events none
-						position absolute
-						top -5px
-						right -5px
-						bottom -5px
-						left -5px
-						border 2px solid rgba($theme-color, 0.3)
-						border-radius 8px
-
-				&.follow
-					color #888
-					background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
-					border solid 1px #e2e2e2
-
-					&:hover
-						background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
-						border-color #dcdcdc
-
-					&:active
-						background #ececec
-						border-color #dcdcdc
-
-				&.unfollow
-					color $theme-color-foreground
-					background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
-					border solid 1px lighten($theme-color, 15%)
-
-					&:not(:disabled)
-						font-weight bold
-
-					&:hover:not(:disabled)
-						background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
-						border-color $theme-color
-
-					&:active:not(:disabled)
-						background $theme-color
-						border-color $theme-color
-
-				&.wait
-					cursor wait !important
-					opacity 0.7
-
-	</style>
-	<script lang="typescript">
-		import isPromise from '../../common/scripts/is-promise';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.user = null;
-		this.userPromise = isPromise(this.opts.user)
-			? this.opts.user
-			: Promise.resolve(this.opts.user);
-		this.init = true;
-		this.wait = false;
-
-		this.on('mount', () => {
-			this.userPromise.then(user => {
-				this.update({
-					init: false,
-					user: user
-				});
-				this.connection.on('follow', this.onStreamFollow);
-				this.connection.on('unfollow', this.onStreamUnfollow);
-			});
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('follow', this.onStreamFollow);
-			this.connection.off('unfollow', this.onStreamUnfollow);
-			this.stream.dispose(this.connectionId);
-		});
-
-		this.onStreamFollow = user => {
-			if (user.id == this.user.id) {
-				this.update({
-					user: user
-				});
-			}
-		};
-
-		this.onStreamUnfollow = user => {
-			if (user.id == this.user.id) {
-				this.update({
-					user: user
-				});
-			}
-		};
-
-		this.onclick = () => {
-			this.wait = true;
-			if (this.user.is_following) {
-				this.api('following/delete', {
-					user_id: this.user.id
-				}).then(() => {
-					this.user.is_following = false;
-				}).catch(err => {
-					console.error(err);
-				}).then(() => {
-					this.wait = false;
-					this.update();
-				});
-			} else {
-				this.api('following/create', {
-					user_id: this.user.id
-				}).then(() => {
-					this.user.is_following = true;
-				}).catch(err => {
-					console.error(err);
-				}).then(() => {
-					this.wait = false;
-					this.update();
-				});
-			}
-		};
-	</script>
-</mk-follow-button>
diff --git a/src/web/app/desktop/views/components/follow-button.vue b/src/web/app/desktop/views/components/follow-button.vue
new file mode 100644
index 000000000..588bcd641
--- /dev/null
+++ b/src/web/app/desktop/views/components/follow-button.vue
@@ -0,0 +1,149 @@
+<template>
+<button class="mk-follow-button"
+	:class="{ wait, follow: !user.is_following, unfollow: user.is_following }"
+	v-if="!init"
+	@click="onClick"
+	:disabled="wait"
+	:title="user.is_following ? 'フォロー解除' : 'フォローする'"
+>
+	<template v-if="!wait && user.is_following">%fa:minus%</template>
+	<template v-if="!wait && !user.is_following">%fa:plus%</template>
+	<template v-if="wait">%fa:spinner .pulse .fw%</template>
+</button>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		user: {
+			type: Object,
+			required: true
+		}
+	},
+	data() {
+		return {
+			wait: false,
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		this.connection = this.$root.$data.os.stream.getConnection();
+		this.connectionId = this.$root.$data.os.stream.use();
+
+		this.connection.on('follow', this.onFollow);
+		this.connection.on('unfollow', this.onUnfollow);
+	},
+	beforeDestroy() {
+		this.connection.off('follow', this.onFollow);
+		this.connection.off('unfollow', this.onUnfollow);
+		this.$root.$data.os.stream.dispose(this.connectionId);
+	},
+	methods: {
+
+		onFollow(user) {
+			if (user.id == this.user.id) {
+				this.user.is_following = user.is_following;
+			}
+		},
+
+		onUnfollow(user) {
+			if (user.id == this.user.id) {
+				this.user.is_following = user.is_following;
+			}
+		},
+
+		onClick() {
+			this.wait = true;
+			if (this.user.is_following) {
+				this.api('following/delete', {
+					user_id: this.user.id
+				}).then(() => {
+					this.user.is_following = false;
+				}).catch(err => {
+					console.error(err);
+				}).then(() => {
+					this.wait = false;
+				});
+			} else {
+				this.api('following/create', {
+					user_id: this.user.id
+				}).then(() => {
+					this.user.is_following = true;
+				}).catch(err => {
+					console.error(err);
+				}).then(() => {
+					this.wait = false;
+				});
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-follow-button
+	display block
+
+	> button
+	> .init
+		display block
+		cursor pointer
+		padding 0
+		margin 0
+		width 32px
+		height 32px
+		font-size 1em
+		outline none
+		border-radius 4px
+
+		*
+			pointer-events none
+
+		&:focus
+			&:after
+				content ""
+				pointer-events none
+				position absolute
+				top -5px
+				right -5px
+				bottom -5px
+				left -5px
+				border 2px solid rgba($theme-color, 0.3)
+				border-radius 8px
+
+		&.follow
+			color #888
+			background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
+			border solid 1px #e2e2e2
+
+			&:hover
+				background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
+				border-color #dcdcdc
+
+			&:active
+				background #ececec
+				border-color #dcdcdc
+
+		&.unfollow
+			color $theme-color-foreground
+			background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+			border solid 1px lighten($theme-color, 15%)
+
+			&:not(:disabled)
+				font-weight bold
+
+			&:hover:not(:disabled)
+				background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
+				border-color $theme-color
+
+			&:active:not(:disabled)
+				background $theme-color
+				border-color $theme-color
+
+		&.wait
+			cursor wait !important
+			opacity 0.7
+
+</style>

From c0c8b0e269a644c0628f5e4bb3f4e93f8fb1bdb2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 17:15:12 +0900
Subject: [PATCH 084/286] wip

---
 src/web/app/desktop/-tags/dialog.tag          | 144 ----------------
 .../app/desktop/views/components/dialog.vue   | 159 ++++++++++++++++++
 2 files changed, 159 insertions(+), 144 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/dialog.tag
 create mode 100644 src/web/app/desktop/views/components/dialog.vue

diff --git a/src/web/app/desktop/-tags/dialog.tag b/src/web/app/desktop/-tags/dialog.tag
deleted file mode 100644
index 9a486dca5..000000000
--- a/src/web/app/desktop/-tags/dialog.tag
+++ /dev/null
@@ -1,144 +0,0 @@
-<mk-dialog>
-	<div class="bg" ref="bg" @click="bgClick"></div>
-	<div class="main" ref="main">
-		<header ref="header"></header>
-		<div class="body" ref="body"></div>
-		<div class="buttons">
-			<template each={ opts.buttons }>
-				<button @click="_onclick">{ text }</button>
-			</template>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> .bg
-				display block
-				position fixed
-				z-index 8192
-				top 0
-				left 0
-				width 100%
-				height 100%
-				background rgba(0, 0, 0, 0.7)
-				opacity 0
-				pointer-events none
-
-			> .main
-				display block
-				position fixed
-				z-index 8192
-				top 20%
-				left 0
-				right 0
-				margin 0 auto 0 auto
-				padding 32px 42px
-				width 480px
-				background #fff
-				opacity 0
-
-				> header
-					margin 1em 0
-					color $theme-color
-					// color #43A4EC
-					font-weight bold
-
-					&:empty
-						display none
-
-					> i
-						margin-right 0.5em
-
-				> .body
-					margin 1em 0
-					color #888
-
-				> .buttons
-					> button
-						display inline-block
-						float right
-						margin 0
-						padding 10px 10px
-						font-size 1.1em
-						font-weight normal
-						text-decoration none
-						color #888
-						background transparent
-						outline none
-						border none
-						border-radius 0
-						cursor pointer
-						transition color 0.1s ease
-
-						i
-							margin 0 0.375em
-
-						&:hover
-							color $theme-color
-
-						&:active
-							color darken($theme-color, 10%)
-							transition color 0s ease
-
-	</style>
-	<script lang="typescript">
-		import * as anime from 'animejs';
-
-		this.canThrough = opts.canThrough != null ? opts.canThrough : true;
-		this.opts.buttons.forEach(button => {
-			button._onclick = () => {
-				if (button.onclick) button.onclick();
-				this.close();
-			};
-		});
-
-		this.on('mount', () => {
-			this.$refs.header.innerHTML = this.opts.title;
-			this.$refs.body.innerHTML = this.opts.text;
-
-			this.$refs.bg.style.pointerEvents = 'auto';
-			anime({
-				targets: this.$refs.bg,
-				opacity: 1,
-				duration: 100,
-				easing: 'linear'
-			});
-
-			anime({
-				targets: this.$refs.main,
-				opacity: 1,
-				scale: [1.2, 1],
-				duration: 300,
-				easing: [ 0, 0.5, 0.5, 1 ]
-			});
-		});
-
-		this.close = () => {
-			this.$refs.bg.style.pointerEvents = 'none';
-			anime({
-				targets: this.$refs.bg,
-				opacity: 0,
-				duration: 300,
-				easing: 'linear'
-			});
-
-			this.$refs.main.style.pointerEvents = 'none';
-			anime({
-				targets: this.$refs.main,
-				opacity: 0,
-				scale: 0.8,
-				duration: 300,
-				easing: [ 0.5, -0.5, 1, 0.5 ],
-				complete: () => this.$destroy()
-			});
-		};
-
-		this.bgClick = () => {
-			if (this.canThrough) {
-				if (this.opts.onThrough) this.opts.onThrough();
-				this.close();
-			}
-		};
-	</script>
-</mk-dialog>
diff --git a/src/web/app/desktop/views/components/dialog.vue b/src/web/app/desktop/views/components/dialog.vue
new file mode 100644
index 000000000..9bb7fca1b
--- /dev/null
+++ b/src/web/app/desktop/views/components/dialog.vue
@@ -0,0 +1,159 @@
+<template>
+<div class="mk-dialog">
+	<div class="bg" ref="bg" @click="onBgClick"></div>
+	<div class="main" ref="main">
+		<header v-html="title"></header>
+		<div class="body" v-html="text"></div>
+		<div class="buttons">
+			<button v-for="(button, i) in buttons" @click="click(button)" :key="i">{{ button.text }}</button>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+
+export default Vue.extend({
+	props: {
+		title: {
+			type: String
+		},
+		text: {
+			type: String
+		},
+		buttons: {
+			type: Array
+		},
+		canThrough: {
+			type: Boolean,
+			default: true
+		},
+		onThrough: {
+			type: Function,
+			required: false
+		}
+	},
+	mounted() {
+		(this.$refs.bg as any).style.pointerEvents = 'auto';
+		anime({
+			targets: this.$refs.bg,
+			opacity: 1,
+			duration: 100,
+			easing: 'linear'
+		});
+
+		anime({
+			targets: this.$refs.main,
+			opacity: 1,
+			scale: [1.2, 1],
+			duration: 300,
+			easing: [0, 0.5, 0.5, 1]
+		});
+	},
+	methods: {
+		click(button) {
+			if (button.onClick) button.onClick();
+			this.close();
+		},
+		close() {
+			(this.$refs.bg as any).style.pointerEvents = 'none';
+			anime({
+				targets: this.$refs.bg,
+				opacity: 0,
+				duration: 300,
+				easing: 'linear'
+			});
+
+			(this.$refs.main as any).style.pointerEvents = 'none';
+			anime({
+				targets: this.$refs.main,
+				opacity: 0,
+				scale: 0.8,
+				duration: 300,
+				easing: [ 0.5, -0.5, 1, 0.5 ],
+				complete: () => this.$destroy()
+			});
+		},
+		onBgClick() {
+			if (this.canThrough) {
+				if (this.onThrough) this.onThrough();
+				this.close();
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-dialog
+	> .bg
+		display block
+		position fixed
+		z-index 8192
+		top 0
+		left 0
+		width 100%
+		height 100%
+		background rgba(0, 0, 0, 0.7)
+		opacity 0
+		pointer-events none
+
+	> .main
+		display block
+		position fixed
+		z-index 8192
+		top 20%
+		left 0
+		right 0
+		margin 0 auto 0 auto
+		padding 32px 42px
+		width 480px
+		background #fff
+		opacity 0
+
+		> header
+			margin 1em 0
+			color $theme-color
+			// color #43A4EC
+			font-weight bold
+
+			&:empty
+				display none
+
+			> i
+				margin-right 0.5em
+
+		> .body
+			margin 1em 0
+			color #888
+
+		> .buttons
+			> button
+				display inline-block
+				float right
+				margin 0
+				padding 10px 10px
+				font-size 1.1em
+				font-weight normal
+				text-decoration none
+				color #888
+				background transparent
+				outline none
+				border none
+				border-radius 0
+				cursor pointer
+				transition color 0.1s ease
+
+				i
+					margin 0 0.375em
+
+				&:hover
+					color $theme-color
+
+				&:active
+					color darken($theme-color, 10%)
+					transition color 0s ease
+
+</style>

From 02def5c3dd9cdae0ce3d00123ee0e048555e0960 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 18:13:31 +0900
Subject: [PATCH 085/286] wip

---
 src/web/app/desktop/-tags/progress-dialog.tag | 97 -------------------
 .../views/components/progress-dialog.vue      | 92 ++++++++++++++++++
 .../views/components/settings-window.vue      |  4 +-
 3 files changed, 94 insertions(+), 99 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/progress-dialog.tag
 create mode 100644 src/web/app/desktop/views/components/progress-dialog.vue

diff --git a/src/web/app/desktop/-tags/progress-dialog.tag b/src/web/app/desktop/-tags/progress-dialog.tag
deleted file mode 100644
index 5df5d7f57..000000000
--- a/src/web/app/desktop/-tags/progress-dialog.tag
+++ /dev/null
@@ -1,97 +0,0 @@
-<mk-progress-dialog>
-	<mk-window ref="window" is-modal={ false } can-close={ false } width={ '500px' }>
-		<yield to="header">{ parent.title }<mk-ellipsis/></yield>
-		<yield to="content">
-			<div class="body">
-				<p class="init" v-if="isNaN(parent.value)">待機中<mk-ellipsis/></p>
-				<p class="percentage" v-if="!isNaN(parent.value)">{ Math.floor((parent.value / parent.max) * 100) }</p>
-				<progress v-if="!isNaN(parent.value) && parent.value < parent.max" value={ isNaN(parent.value) ? 0 : parent.value } max={ parent.max }></progress>
-				<div class="progress waiting" v-if="parent.value >= parent.max"></div>
-			</div>
-		</yield>
-	</mk-window>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> mk-window
-				[data-yield='content']
-
-					> .body
-						padding 18px 24px 24px 24px
-
-						> .init
-							display block
-							margin 0
-							text-align center
-							color rgba(#000, 0.7)
-
-						> .percentage
-							display block
-							margin 0 0 4px 0
-							text-align center
-							line-height 16px
-							color rgba($theme-color, 0.7)
-
-							&:after
-								content '%'
-
-						> progress
-						> .progress
-							display block
-							margin 0
-							width 100%
-							height 10px
-							background transparent
-							border none
-							border-radius 4px
-							overflow hidden
-
-							&::-webkit-progress-value
-								background $theme-color
-
-							&::-webkit-progress-bar
-								background rgba($theme-color, 0.1)
-
-						> .progress
-							background linear-gradient(
-								45deg,
-								lighten($theme-color, 30%) 25%,
-								$theme-color               25%,
-								$theme-color               50%,
-								lighten($theme-color, 30%) 50%,
-								lighten($theme-color, 30%) 75%,
-								$theme-color               75%,
-								$theme-color
-							)
-							background-size 32px 32px
-							animation progress-dialog-tag-progress-waiting 1.5s linear infinite
-
-							@keyframes progress-dialog-tag-progress-waiting
-								from {background-position: 0 0;}
-								to   {background-position: -64px 32px;}
-
-	</style>
-	<script lang="typescript">
-		this.title = this.opts.title;
-		this.value = parseInt(this.opts.value, 10);
-		this.max = parseInt(this.opts.max, 10);
-
-		this.on('mount', () => {
-			this.$refs.window.on('closed', () => {
-				this.$destroy();
-			});
-		});
-
-		this.updateProgress = (value, max) => {
-			this.update({
-				value: parseInt(value, 10),
-				max: parseInt(max, 10)
-			});
-		};
-
-		this.close = () => {
-			this.$refs.window.close();
-		};
-	</script>
-</mk-progress-dialog>
diff --git a/src/web/app/desktop/views/components/progress-dialog.vue b/src/web/app/desktop/views/components/progress-dialog.vue
new file mode 100644
index 000000000..9a925d5b1
--- /dev/null
+++ b/src/web/app/desktop/views/components/progress-dialog.vue
@@ -0,0 +1,92 @@
+<template>
+<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="$destroy">
+	<span to="header">{{ title }}<mk-ellipsis/></span>
+	<div to="content">
+		<div :class="$style.body">
+			<p :class="$style.init" v-if="isNaN(value)">待機中<mk-ellipsis/></p>
+			<p :class="$style.percentage" v-if="!isNaN(value)">{{ Math.floor((value / max) * 100) }}</p>
+			<progress :class="$style.progress"
+				v-if="!isNaN(value) && value < max"
+				:value="isNaN(value) ? 0 : value"
+				:max="max"
+			></progress>
+			<div :class="[$style.progress, $style.waiting]" v-if="value >= max"></div>
+		</div>
+	</div>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['title', 'initValue', 'initMax'],
+	data() {
+		return {
+			value: this.initValue,
+			max: this.initMax
+		};
+	},
+	methods: {
+		update(value, max) {
+			this.value = parseInt(value, 10);
+			this.max = parseInt(max, 10);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.body
+	padding 18px 24px 24px 24px
+
+.init
+	display block
+	margin 0
+	text-align center
+	color rgba(#000, 0.7)
+
+.percentage
+	display block
+	margin 0 0 4px 0
+	text-align center
+	line-height 16px
+	color rgba($theme-color, 0.7)
+
+	&:after
+		content '%'
+
+.progress
+	display block
+	margin 0
+	width 100%
+	height 10px
+	background transparent
+	border none
+	border-radius 4px
+	overflow hidden
+
+	&::-webkit-progress-value
+		background $theme-color
+
+	&::-webkit-progress-bar
+		background rgba($theme-color, 0.1)
+
+.waiting
+	background linear-gradient(
+		45deg,
+		lighten($theme-color, 30%) 25%,
+		$theme-color               25%,
+		$theme-color               50%,
+		lighten($theme-color, 30%) 50%,
+		lighten($theme-color, 30%) 75%,
+		$theme-color               75%,
+		$theme-color
+	)
+	background-size 32px 32px
+	animation progress-dialog-tag-progress-waiting 1.5s linear infinite
+
+	@keyframes progress-dialog-tag-progress-waiting
+		from {background-position: 0 0;}
+		to   {background-position: -64px 32px;}
+
+</style>
diff --git a/src/web/app/desktop/views/components/settings-window.vue b/src/web/app/desktop/views/components/settings-window.vue
index 56d839851..074bd2e24 100644
--- a/src/web/app/desktop/views/components/settings-window.vue
+++ b/src/web/app/desktop/views/components/settings-window.vue
@@ -1,7 +1,7 @@
 <template>
-<mk-window ref="window" is-modal width='700px' height='550px' @closed="$destroy">
+<mk-window is-modal width='700px' height='550px' @closed="$destroy">
 	<span slot="header" :class="$style.header">%fa:cog%設定</span>
-	<div to="content">
+	<div slot="content">
 		<mk-settings/>
 	</div>
 </mk-window>

From e79b132b91cb758bd52422ec7e6b64a5b14668c7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 19:00:54 +0900
Subject: [PATCH 086/286] wip

---
 src/web/app/desktop/-tags/input-dialog.tag    | 172 ----------------
 .../desktop/views/components/input-dialog.vue | 183 ++++++++++++++++++
 .../app/desktop/views/components/window.vue   |   2 +-
 3 files changed, 184 insertions(+), 173 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/input-dialog.tag
 create mode 100644 src/web/app/desktop/views/components/input-dialog.vue

diff --git a/src/web/app/desktop/-tags/input-dialog.tag b/src/web/app/desktop/-tags/input-dialog.tag
deleted file mode 100644
index a1634429c..000000000
--- a/src/web/app/desktop/-tags/input-dialog.tag
+++ /dev/null
@@ -1,172 +0,0 @@
-<mk-input-dialog>
-	<mk-window ref="window" is-modal={ true } width={ '500px' }>
-		<yield to="header">
-			%fa:i-cursor%{ parent.title }
-		</yield>
-		<yield to="content">
-			<div class="body">
-				<input ref="text" type={ parent.type } oninput={ parent.onInput } onkeydown={ parent.onKeydown } placeholder={ parent.placeholder }/>
-			</div>
-			<div class="action">
-				<button class="cancel" @click="parent.cancel">キャンセル</button>
-				<button class="ok" disabled={ !parent.allowEmpty && refs.text.value.length == 0 } @click="parent.ok">決定</button>
-			</div>
-		</yield>
-	</mk-window>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> mk-window
-				[data-yield='header']
-					> [data-fa]
-						margin-right 4px
-
-				[data-yield='content']
-					> .body
-						padding 16px
-
-						> input
-							display block
-							padding 8px
-							margin 0
-							width 100%
-							max-width 100%
-							min-width 100%
-							font-size 1em
-							color #333
-							background #fff
-							outline none
-							border solid 1px rgba($theme-color, 0.1)
-							border-radius 4px
-							transition border-color .3s ease
-
-							&:hover
-								border-color rgba($theme-color, 0.2)
-								transition border-color .1s ease
-
-							&:focus
-								color $theme-color
-								border-color rgba($theme-color, 0.5)
-								transition border-color 0s ease
-
-							&::-webkit-input-placeholder
-								color rgba($theme-color, 0.3)
-
-					> .action
-						height 72px
-						background lighten($theme-color, 95%)
-
-						.ok
-						.cancel
-							display block
-							position absolute
-							bottom 16px
-							cursor pointer
-							padding 0
-							margin 0
-							width 120px
-							height 40px
-							font-size 1em
-							outline none
-							border-radius 4px
-
-							&:focus
-								&:after
-									content ""
-									pointer-events none
-									position absolute
-									top -5px
-									right -5px
-									bottom -5px
-									left -5px
-									border 2px solid rgba($theme-color, 0.3)
-									border-radius 8px
-
-							&:disabled
-								opacity 0.7
-								cursor default
-
-						.ok
-							right 16px
-							color $theme-color-foreground
-							background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
-							border solid 1px lighten($theme-color, 15%)
-
-							&:not(:disabled)
-								font-weight bold
-
-							&:hover:not(:disabled)
-								background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
-								border-color $theme-color
-
-							&:active:not(:disabled)
-								background $theme-color
-								border-color $theme-color
-
-						.cancel
-							right 148px
-							color #888
-							background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
-							border solid 1px #e2e2e2
-
-							&:hover
-								background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
-								border-color #dcdcdc
-
-							&:active
-								background #ececec
-								border-color #dcdcdc
-
-	</style>
-	<script lang="typescript">
-		this.done = false;
-
-		this.title = this.opts.title;
-		this.placeholder = this.opts.placeholder;
-		this.default = this.opts.default;
-		this.allowEmpty = this.opts.allowEmpty != null ? this.opts.allowEmpty : true;
-		this.type = this.opts.type ? this.opts.type : 'text';
-
-		this.on('mount', () => {
-			this.text = this.$refs.window.refs.text;
-			if (this.default) this.text.value = this.default;
-			this.text.focus();
-
-			this.$refs.window.on('closing', () => {
-				if (this.done) {
-					this.opts.onOk(this.text.value);
-				} else {
-					if (this.opts.onCancel) this.opts.onCancel();
-				}
-			});
-
-			this.$refs.window.on('closed', () => {
-				this.$destroy();
-			});
-		});
-
-		this.cancel = () => {
-			this.done = false;
-			this.$refs.window.close();
-		};
-
-		this.ok = () => {
-			if (!this.allowEmpty && this.text.value == '') return;
-			this.done = true;
-			this.$refs.window.close();
-		};
-
-		this.onInput = () => {
-			this.update();
-		};
-
-		this.onKeydown = e => {
-			if (e.which == 13) { // Enter
-				e.preventDefault();
-				e.stopPropagation();
-				this.ok();
-			}
-		};
-	</script>
-</mk-input-dialog>
diff --git a/src/web/app/desktop/views/components/input-dialog.vue b/src/web/app/desktop/views/components/input-dialog.vue
new file mode 100644
index 000000000..684698a0f
--- /dev/null
+++ b/src/web/app/desktop/views/components/input-dialog.vue
@@ -0,0 +1,183 @@
+<template>
+<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="$destroy">
+	<span slot="header" :class="$style.header">
+		%fa:i-cursor%{{ title }}
+	</span>
+	<div slot="content">
+		<div :class="$style.body">
+			<input ref="text" v-model="text" :type="type" @keydown="onKeydown" :placeholder="placeholder"/>
+		</div>
+		<div :class="$style.actions">
+			<button :class="$style.cancel" @click="cancel">キャンセル</button>
+			<button :class="$style.ok" disabled="!parent.allowEmpty && text.length == 0" @click="ok">決定</button>
+		</div>
+	</div>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		title: {
+			type: String
+		},
+		placeholder: {
+			type: String
+		},
+		default: {
+			type: String
+		},
+		allowEmpty: {
+			default: true
+		},
+		type: {
+			default: 'text'
+		},
+		onOk: {
+			type: Function
+		},
+		onCancel: {
+			type: Function
+		}
+	},
+	data() {
+		return {
+			done: false,
+			text: ''
+		};
+	},
+	mounted() {
+		if (this.default) this.text = this.default;
+		(this.$refs.text as any).focus();
+	},
+	methods: {
+		ok() {
+			if (!this.allowEmpty && this.text == '') return;
+			this.done = true;
+			(this.$refs.window as any).close();
+		},
+		cancel() {
+			this.done = false;
+			(this.$refs.window as any).close();
+		},
+		beforeClose() {
+			if (this.done) {
+				this.onOk(this.text);
+			} else {
+				if (this.onCancel) this.onCancel();
+			}
+		},
+		onKeydown(e) {
+			if (e.which == 13) { // Enter
+				e.preventDefault();
+				e.stopPropagation();
+				this.ok();
+			}
+		}
+	}
+});
+</script>
+
+
+<style lang="stylus" module>
+.header
+	> [data-fa]
+		margin-right 4px
+
+.body
+	padding 16px
+
+	> input
+		display block
+		padding 8px
+		margin 0
+		width 100%
+		max-width 100%
+		min-width 100%
+		font-size 1em
+		color #333
+		background #fff
+		outline none
+		border solid 1px rgba($theme-color, 0.1)
+		border-radius 4px
+		transition border-color .3s ease
+
+		&:hover
+			border-color rgba($theme-color, 0.2)
+			transition border-color .1s ease
+
+		&:focus
+			color $theme-color
+			border-color rgba($theme-color, 0.5)
+			transition border-color 0s ease
+
+		&::-webkit-input-placeholder
+			color rgba($theme-color, 0.3)
+
+.actions
+	height 72px
+	background lighten($theme-color, 95%)
+
+.ok
+.cancel
+	display block
+	position absolute
+	bottom 16px
+	cursor pointer
+	padding 0
+	margin 0
+	width 120px
+	height 40px
+	font-size 1em
+	outline none
+	border-radius 4px
+
+	&:focus
+		&:after
+			content ""
+			pointer-events none
+			position absolute
+			top -5px
+			right -5px
+			bottom -5px
+			left -5px
+			border 2px solid rgba($theme-color, 0.3)
+			border-radius 8px
+
+	&:disabled
+		opacity 0.7
+		cursor default
+
+.ok
+	right 16px
+	color $theme-color-foreground
+	background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+	border solid 1px lighten($theme-color, 15%)
+
+	&:not(:disabled)
+		font-weight bold
+
+	&:hover:not(:disabled)
+		background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
+		border-color $theme-color
+
+	&:active:not(:disabled)
+		background $theme-color
+		border-color $theme-color
+
+.cancel
+	right 148px
+	color #888
+	background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
+	border solid 1px #e2e2e2
+
+	&:hover
+		background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
+		border-color #dcdcdc
+
+	&:active
+		background #ececec
+		border-color #dcdcdc
+
+</style>
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 61a433b36..3a7531a6f 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -134,7 +134,7 @@ export default Vue.extend({
 		},
 
 		close() {
-			this.$emit('closing');
+			this.$emit('before-close');
 
 			const bg = this.$refs.bg as any;
 			const main = this.$refs.main as any;

From d920e84c7142280c5419f0173c5e6c691f5064d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 19:03:24 +0900
Subject: [PATCH 087/286] wip

---
 src/web/app/desktop/views/components/input-dialog.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/desktop/views/components/input-dialog.vue b/src/web/app/desktop/views/components/input-dialog.vue
index 684698a0f..a78c7dcba 100644
--- a/src/web/app/desktop/views/components/input-dialog.vue
+++ b/src/web/app/desktop/views/components/input-dialog.vue
@@ -9,7 +9,7 @@
 		</div>
 		<div :class="$style.actions">
 			<button :class="$style.cancel" @click="cancel">キャンセル</button>
-			<button :class="$style.ok" disabled="!parent.allowEmpty && text.length == 0" @click="ok">決定</button>
+			<button :class="$style.ok" disabled="!allowEmpty && text.length == 0" @click="ok">決定</button>
 		</div>
 	</div>
 </mk-window>

From ec509b5179739cab20808cb1ca31437c05e2eda7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 19:20:40 +0900
Subject: [PATCH 088/286] wip

---
 .../app/desktop/-tags/following-setuper.tag   | 169 ------------------
 .../views/components/friends-maker.vue        | 168 +++++++++++++++++
 2 files changed, 168 insertions(+), 169 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/following-setuper.tag
 create mode 100644 src/web/app/desktop/views/components/friends-maker.vue

diff --git a/src/web/app/desktop/-tags/following-setuper.tag b/src/web/app/desktop/-tags/following-setuper.tag
deleted file mode 100644
index 75ce76ae5..000000000
--- a/src/web/app/desktop/-tags/following-setuper.tag
+++ /dev/null
@@ -1,169 +0,0 @@
-<mk-following-setuper>
-	<p class="title">気になるユーザーをフォロー:</p>
-	<div class="users" v-if="!fetching && users.length > 0">
-		<div class="user" each={ users }><a class="avatar-anchor" href={ '/' + username }><img class="avatar" src={ avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ id }/></a>
-			<div class="body"><a class="name" href={ '/' + username } target="_blank" data-user-preview={ id }>{ name }</a>
-				<p class="username">@{ username }</p>
-			</div>
-			<mk-follow-button user={ this }/>
-		</div>
-	</div>
-	<p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p>
-	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
-	<a class="refresh" @click="refresh">もっと見る</a>
-	<button class="close" @click="close" title="閉じる">%fa:times%</button>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding 24px
-
-			> .title
-				margin 0 0 12px 0
-				font-size 1em
-				font-weight bold
-				color #888
-
-			> .users
-				&:after
-					content ""
-					display block
-					clear both
-
-				> .user
-					padding 16px
-					width 238px
-					float left
-
-					&:after
-						content ""
-						display block
-						clear both
-
-					> .avatar-anchor
-						display block
-						float left
-						margin 0 12px 0 0
-
-						> .avatar
-							display block
-							width 42px
-							height 42px
-							margin 0
-							border-radius 8px
-							vertical-align bottom
-
-					> .body
-						float left
-						width calc(100% - 54px)
-
-						> .name
-							margin 0
-							font-size 16px
-							line-height 24px
-							color #555
-
-						> .username
-							margin 0
-							font-size 15px
-							line-height 16px
-							color #ccc
-
-					> mk-follow-button
-						position absolute
-						top 16px
-						right 16px
-
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-			> .fetching
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-			> .refresh
-				display block
-				margin 0 8px 0 0
-				text-align right
-				font-size 0.9em
-				color #999
-
-			> .close
-				cursor pointer
-				display block
-				position absolute
-				top 6px
-				right 6px
-				z-index 1
-				margin 0
-				padding 0
-				font-size 1.2em
-				color #999
-				border none
-				outline none
-				background transparent
-
-				&:hover
-					color #555
-
-				&:active
-					color #222
-
-				> [data-fa]
-					padding 14px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-		this.mixin('user-preview');
-
-		this.users = null;
-		this.fetching = true;
-
-		this.limit = 6;
-		this.page = 0;
-
-		this.on('mount', () => {
-			this.fetch();
-		});
-
-		this.fetch = () => {
-			this.update({
-				fetching: true,
-				users: null
-			});
-
-			this.api('users/recommendation', {
-				limit: this.limit,
-				offset: this.limit * this.page
-			}).then(users => {
-				this.fetching = false
-				this.users = users
-				this.update({
-					fetching: false,
-					users: users
-				});
-			});
-		};
-
-		this.refresh = () => {
-			if (this.users.length < this.limit) {
-				this.page = 0;
-			} else {
-				this.page++;
-			}
-			this.fetch();
-		};
-
-		this.close = () => {
-			this.$destroy();
-		};
-	</script>
-</mk-following-setuper>
diff --git a/src/web/app/desktop/views/components/friends-maker.vue b/src/web/app/desktop/views/components/friends-maker.vue
new file mode 100644
index 000000000..add6c10a3
--- /dev/null
+++ b/src/web/app/desktop/views/components/friends-maker.vue
@@ -0,0 +1,168 @@
+<template>
+<div class="mk-friends-maker">
+	<p class="title">気になるユーザーをフォロー:</p>
+	<div class="users" v-if="!fetching && users.length > 0">
+		<div class="user" v-for="user in users" :key="user.id">
+			<a class="avatar-anchor" :href="`/${user.username}`">
+				<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="user.id"/>
+			</a>
+			<div class="body">
+				<a class="name" :href="`/${user.username}`" target="_blank" v-user-preview="user.id">{{ user.name }}</a>
+				<p class="username">@{{ user.username }}</p>
+			</div>
+			<mk-follow-button user="user"/>
+		</div>
+	</div>
+	<p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
+	<a class="refresh" @click="refresh">もっと見る</a>
+	<button class="close" @click="$destroy" title="閉じる">%fa:times%</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			users: [],
+			fetching: true,
+			limit: 6,
+			page: 0
+		};
+	},
+	mounted() {
+		this.fetch();
+	},
+	methods: {
+		fetch() {
+			this.fetching = true;
+			this.users = [];
+
+			this.$root.$data.os.api('users/recommendation', {
+				limit: this.limit,
+				offset: this.limit * this.page
+			}).then(users => {
+				this.fetching = false;
+				this.users = users;
+			});
+		},
+		refresh() {
+			if (this.users.length < this.limit) {
+				this.page = 0;
+			} else {
+				this.page++;
+			}
+			this.fetch();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-friends-maker
+	padding 24px
+
+	> .title
+		margin 0 0 12px 0
+		font-size 1em
+		font-weight bold
+		color #888
+
+	> .users
+		&:after
+			content ""
+			display block
+			clear both
+
+		> .user
+			padding 16px
+			width 238px
+			float left
+
+			&:after
+				content ""
+				display block
+				clear both
+
+			> .avatar-anchor
+				display block
+				float left
+				margin 0 12px 0 0
+
+				> .avatar
+					display block
+					width 42px
+					height 42px
+					margin 0
+					border-radius 8px
+					vertical-align bottom
+
+			> .body
+				float left
+				width calc(100% - 54px)
+
+				> .name
+					margin 0
+					font-size 16px
+					line-height 24px
+					color #555
+
+				> .username
+					margin 0
+					font-size 15px
+					line-height 16px
+					color #ccc
+
+			> mk-follow-button
+				position absolute
+				top 16px
+				right 16px
+
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+	> .fetching
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+	> .refresh
+		display block
+		margin 0 8px 0 0
+		text-align right
+		font-size 0.9em
+		color #999
+
+	> .close
+		cursor pointer
+		display block
+		position absolute
+		top 6px
+		right 6px
+		z-index 1
+		margin 0
+		padding 0
+		font-size 1.2em
+		color #999
+		border none
+		outline none
+		background transparent
+
+		&:hover
+			color #555
+
+		&:active
+			color #222
+
+		> [data-fa]
+			padding 14px
+
+</style>

From 106b7a0cef40125a4c7ce7c737e0704e71c518f9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 20:53:45 +0900
Subject: [PATCH 089/286] wip

---
 .../views/components/messaging-room.vue       |   4 +-
 src/web/app/desktop/-tags/notifications.tag   | 301 -----------------
 .../views/components/notifications.vue        | 315 ++++++++++++++++++
 3 files changed, 317 insertions(+), 303 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/notifications.tag
 create mode 100644 src/web/app/desktop/views/components/notifications.vue

diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index 2fb6671b8..838e1e265 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -7,9 +7,9 @@
 		<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="moreMessagesIsInStock" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
 			<template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' }}
 		</button>
-		<template v-for="(message, i) in messages">
+		<template v-for="(message, i) in _messages">
 			<mk-messaging-message :message="message" :key="message.id"/>
-			<p class="date" :key="message.id + '-time'" v-if="i != messages.length - 1 && _message._date != _messages[i + 1]._date"><span>{{ _messages[i + 1]._datetext }}</span></p>
+			<p class="date" :key="message.id + '-time'" v-if="i != messages.length - 1 && message._date != _messages[i + 1]._date"><span>{{ _messages[i + 1]._datetext }}</span></p>
 		</template>
 	</div>
 	<footer>
diff --git a/src/web/app/desktop/-tags/notifications.tag b/src/web/app/desktop/-tags/notifications.tag
deleted file mode 100644
index a599e5d6a..000000000
--- a/src/web/app/desktop/-tags/notifications.tag
+++ /dev/null
@@ -1,301 +0,0 @@
-<mk-notifications>
-	<div class="notifications" v-if="notifications.length != 0">
-		<template each={ notification, i in notifications }>
-			<div class="notification { notification.type }">
-				<mk-time time={ notification.created_at }/>
-				<template v-if="notification.type == 'reaction'">
-					<a class="avatar-anchor" href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>
-						<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
-					</a>
-					<div class="text">
-						<p><mk-reaction-icon reaction={ notification.reaction }/><a href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>{ notification.user.name }</a></p>
-						<a class="post-ref" href={ '/' + notification.post.user.username + '/' + notification.post.id }>
-							%fa:quote-left%{ getPostSummary(notification.post) }%fa:quote-right%
-						</a>
-					</div>
-				</template>
-				<template v-if="notification.type == 'repost'">
-					<a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>
-						<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
-					</a>
-					<div class="text">
-						<p>%fa:retweet%<a href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>{ notification.post.user.name }</a></p>
-						<a class="post-ref" href={ '/' + notification.post.user.username + '/' + notification.post.id }>
-							%fa:quote-left%{ getPostSummary(notification.post.repost) }%fa:quote-right%
-						</a>
-					</div>
-				</template>
-				<template v-if="notification.type == 'quote'">
-					<a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>
-						<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
-					</a>
-					<div class="text">
-						<p>%fa:quote-left%<a href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>{ notification.post.user.name }</a></p>
-						<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
-					</div>
-				</template>
-				<template v-if="notification.type == 'follow'">
-					<a class="avatar-anchor" href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>
-						<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
-					</a>
-					<div class="text">
-						<p>%fa:user-plus%<a href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>{ notification.user.name }</a></p>
-					</div>
-				</template>
-				<template v-if="notification.type == 'reply'">
-					<a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>
-						<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
-					</a>
-					<div class="text">
-						<p>%fa:reply%<a href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>{ notification.post.user.name }</a></p>
-						<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
-					</div>
-				</template>
-				<template v-if="notification.type == 'mention'">
-					<a class="avatar-anchor" href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>
-						<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
-					</a>
-					<div class="text">
-						<p>%fa:at%<a href={ '/' + notification.post.user.username } data-user-preview={ notification.post.user_id }>{ notification.post.user.name }</a></p>
-						<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
-					</div>
-				</template>
-				<template v-if="notification.type == 'poll_vote'">
-					<a class="avatar-anchor" href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>
-						<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=48' } alt="avatar"/>
-					</a>
-					<div class="text">
-						<p>%fa:chart-pie%<a href={ '/' + notification.user.username } data-user-preview={ notification.user.id }>{ notification.user.name }</a></p>
-						<a class="post-ref" href={ '/' + notification.post.user.username + '/' + notification.post.id }>
-							%fa:quote-left%{ getPostSummary(notification.post) }%fa:quote-right%
-						</a>
-					</div>
-				</template>
-			</div>
-			<p class="date" v-if="i != notifications.length - 1 && notification._date != notifications[i + 1]._date">
-				<span>%fa:angle-up%{ notification._datetext }</span>
-				<span>%fa:angle-down%{ notifications[i + 1]._datetext }</span>
-			</p>
-		</template>
-	</div>
-	<button class="more { fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" disabled={ fetchingMoreNotifications }>
-		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:desktop.tags.mk-notifications.more%' }
-	</button>
-	<p class="empty" v-if="notifications.length == 0 && !loading">ありません!</p>
-	<p class="loading" v-if="loading">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> .notifications
-				> .notification
-					margin 0
-					padding 16px
-					overflow-wrap break-word
-					font-size 0.9em
-					border-bottom solid 1px rgba(0, 0, 0, 0.05)
-
-					&:last-child
-						border-bottom none
-
-					> mk-time
-						display inline
-						position absolute
-						top 16px
-						right 12px
-						vertical-align top
-						color rgba(0, 0, 0, 0.6)
-						font-size small
-
-					&:after
-						content ""
-						display block
-						clear both
-
-					> .avatar-anchor
-						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
-
-					> .text
-						float right
-						width calc(100% - 36px)
-						padding-left 8px
-
-						p
-							margin 0
-
-							i, mk-reaction-icon
-								margin-right 4px
-
-					.post-preview
-						color rgba(0, 0, 0, 0.7)
-
-					.post-ref
-						color rgba(0, 0, 0, 0.7)
-
-						[data-fa]
-							font-size 1em
-							font-weight normal
-							font-style normal
-							display inline-block
-							margin-right 3px
-
-					&.repost, &.quote
-						.text p i
-							color #77B255
-
-					&.follow
-						.text p i
-							color #53c7ce
-
-					&.reply, &.mention
-						.text p i
-							color #555
-
-				> .date
-					display block
-					margin 0
-					line-height 32px
-					text-align center
-					font-size 0.8em
-					color #aaa
-					background #fdfdfd
-					border-bottom solid 1px rgba(0, 0, 0, 0.05)
-
-					span
-						margin 0 16px
-
-					[data-fa]
-						margin-right 8px
-
-			> .more
-				display block
-				width 100%
-				padding 16px
-				color #555
-				border-top solid 1px rgba(0, 0, 0, 0.05)
-
-				&:hover
-					background rgba(0, 0, 0, 0.025)
-
-				&:active
-					background rgba(0, 0, 0, 0.05)
-
-				&.fetching
-					cursor wait
-
-				> [data-fa]
-					margin-right 4px
-
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-			> .loading
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		import getPostSummary from '../../../../common/get-post-summary.ts';
-		this.getPostSummary = getPostSummary;
-
-		this.mixin('i');
-		this.mixin('api');
-		this.mixin('user-preview');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.notifications = [];
-		this.loading = true;
-
-		this.on('mount', () => {
-			const max = 10;
-
-			this.api('i/notifications', {
-				limit: max + 1
-			}).then(notifications => {
-				if (notifications.length == max + 1) {
-					this.moreNotifications = true;
-					notifications.pop();
-				}
-
-				this.update({
-					loading: false,
-					notifications: notifications
-				});
-			});
-
-			this.connection.on('notification', this.onNotification);
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('notification', this.onNotification);
-			this.stream.dispose(this.connectionId);
-		});
-
-		this.on('update', () => {
-			this.notifications.forEach(notification => {
-				const date = new Date(notification.created_at).getDate();
-				const month = new Date(notification.created_at).getMonth() + 1;
-				notification._date = date;
-				notification._datetext = `${month}月 ${date}日`;
-			});
-		});
-
-		this.onNotification = notification => {
-			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
-			this.connection.send({
-				type: 'read_notification',
-				id: notification.id
-			});
-
-			this.notifications.unshift(notification);
-			this.update();
-		};
-
-		this.fetchMoreNotifications = () => {
-			this.update({
-				fetchingMoreNotifications: true
-			});
-
-			const max = 30;
-
-			this.api('i/notifications', {
-				limit: max + 1,
-				until_id: this.notifications[this.notifications.length - 1].id
-			}).then(notifications => {
-				if (notifications.length == max + 1) {
-					this.moreNotifications = true;
-					notifications.pop();
-				} else {
-					this.moreNotifications = false;
-				}
-				this.update({
-					notifications: this.notifications.concat(notifications),
-					fetchingMoreNotifications: false
-				});
-			});
-		};
-	</script>
-</mk-notifications>
diff --git a/src/web/app/desktop/views/components/notifications.vue b/src/web/app/desktop/views/components/notifications.vue
new file mode 100644
index 000000000..5826fc210
--- /dev/null
+++ b/src/web/app/desktop/views/components/notifications.vue
@@ -0,0 +1,315 @@
+<template>
+<div class="mk-notifications">
+	<div class="notifications" v-if="notifications.length != 0">
+		<template v-for="(notification, i) in _notifications">
+			<div class="notification" :class="notification.type" :key="notification.id">
+				<mk-time :time="notification.created_at"/>
+				<template v-if="notification.type == 'reaction'">
+					<a class="avatar-anchor" :href="`/${notification.user.username}`" :v-user-preview="notification.user.id">
+						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+					</a>
+					<div class="text">
+						<p>
+							<mk-reaction-icon reaction={ notification.reaction }/>
+							<a :href="`/${notification.user.username}`" :v-user-preview="notification.user.id">{{ notification.user.name }}</a>
+						</p>
+						<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
+							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
+						</a>
+					</div>
+				</template>
+				<template v-if="notification.type == 'repost'">
+					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">
+						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+					</a>
+					<div class="text">
+						<p>%fa:retweet%
+							<a :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
+						</p>
+						<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
+							%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
+						</a>
+					</div>
+				</template>
+				<template v-if="notification.type == 'quote'">
+					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">
+						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+					</a>
+					<div class="text">
+						<p>%fa:quote-left%
+							<a :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
+						</p>
+						<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
+					</div>
+				</template>
+				<template v-if="notification.type == 'follow'">
+					<a class="avatar-anchor" :href="`/${notification.user.username}`" :v-user-preview="notification.user.id">
+						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+					</a>
+					<div class="text">
+						<p>%fa:user-plus%
+							<a :href="`/${notification.user.username}`" :v-user-preview="notification.user.id">{{ notification.user.name }}</a>
+						</p>
+					</div>
+				</template>
+				<template v-if="notification.type == 'reply'">
+					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">
+						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+					</a>
+					<div class="text">
+						<p>%fa:reply%
+							<a :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
+						</p>
+						<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
+					</div>
+				</template>
+				<template v-if="notification.type == 'mention'">
+					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">
+						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+					</a>
+					<div class="text">
+						<p>%fa:at%
+							<a :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
+						</p>
+						<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
+					</div>
+				</template>
+				<template v-if="notification.type == 'poll_vote'">
+					<a class="avatar-anchor" :href="`/${notification.user.username}`" :v-user-preview="notification.user.id">
+						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
+					</a>
+					<div class="text">
+						<p>%fa:chart-pie%<a :href="`/${notification.user.username}`" :v-user-preview="notification.user.id">{{ notification.user.name }}</a></p>
+						<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
+							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
+						</a>
+					</div>
+				</template>
+			</div>
+			<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
+				<span>%fa:angle-up%{{ notification._datetext }}</span>
+				<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
+			</p>
+		</template>
+	</div>
+	<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
+		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:desktop.tags.mk-notifications.more%' }}
+	</button>
+	<p class="empty" v-if="notifications.length == 0 && !fetching">ありません!</p>
+	<p class="loading" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import getPostSummary from '../../../../../common/get-post-summary';
+
+export default Vue.extend({
+	data() {
+		return {
+			fetching: true,
+			fetchingMoreNotifications: false,
+			notifications: [],
+			moreNotifications: false,
+			connection: null,
+			connectionId: null,
+			getPostSummary
+		};
+	},
+	computed: {
+		_notifications(): any[] {
+			return (this.notifications as any).map(notification => {
+				const date = new Date(notification.created_at).getDate();
+				const month = new Date(notification.created_at).getMonth() + 1;
+				notification._date = date;
+				notification._datetext = `${month}月 ${date}日`;
+				return notification;
+			});
+		}
+	},
+	mounted() {
+		this.connection = this.$root.$data.os.stream.getConnection();
+		this.connectionId = this.$root.$data.os.stream.use();
+
+		this.connection.on('notification', this.onNotification);
+
+		const max = 10;
+
+		this.$root.$data.os.api('i/notifications', {
+			limit: max + 1
+		}).then(notifications => {
+			if (notifications.length == max + 1) {
+				this.moreNotifications = true;
+				notifications.pop();
+			}
+
+			this.notifications = notifications;
+			this.fetching = false;
+		});
+	},
+	beforeDestroy() {
+		this.connection.off('notification', this.onNotification);
+		this.$root.$data.os.stream.dispose(this.connectionId);
+	},
+	methods: {
+		fetchMoreNotifications() {
+			this.fetchingMoreNotifications = true;
+
+			const max = 30;
+
+			this.$root.$data.os.api('i/notifications', {
+				limit: max + 1,
+				until_id: this.notifications[this.notifications.length - 1].id
+			}).then(notifications => {
+				if (notifications.length == max + 1) {
+					this.moreNotifications = true;
+					notifications.pop();
+				} else {
+					this.moreNotifications = false;
+				}
+				this.notifications = this.notifications.concat(notifications);
+				this.fetchingMoreNotifications = false;
+			});
+		},
+		onNotification(notification) {
+			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
+			this.connection.send({
+				type: 'read_notification',
+				id: notification.id
+			});
+
+			this.notifications.unshift(notification);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-notifications
+	> .notifications
+		> .notification
+			margin 0
+			padding 16px
+			overflow-wrap break-word
+			font-size 0.9em
+			border-bottom solid 1px rgba(0, 0, 0, 0.05)
+
+			&:last-child
+				border-bottom none
+
+			> mk-time
+				display inline
+				position absolute
+				top 16px
+				right 12px
+				vertical-align top
+				color rgba(0, 0, 0, 0.6)
+				font-size small
+
+			&:after
+				content ""
+				display block
+				clear both
+
+			> .avatar-anchor
+				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
+
+			> .text
+				float right
+				width calc(100% - 36px)
+				padding-left 8px
+
+				p
+					margin 0
+
+					i, mk-reaction-icon
+						margin-right 4px
+
+			.post-preview
+				color rgba(0, 0, 0, 0.7)
+
+			.post-ref
+				color rgba(0, 0, 0, 0.7)
+
+				[data-fa]
+					font-size 1em
+					font-weight normal
+					font-style normal
+					display inline-block
+					margin-right 3px
+
+			&.repost, &.quote
+				.text p i
+					color #77B255
+
+			&.follow
+				.text p i
+					color #53c7ce
+
+			&.reply, &.mention
+				.text p i
+					color #555
+
+		> .date
+			display block
+			margin 0
+			line-height 32px
+			text-align center
+			font-size 0.8em
+			color #aaa
+			background #fdfdfd
+			border-bottom solid 1px rgba(0, 0, 0, 0.05)
+
+			span
+				margin 0 16px
+
+			[data-fa]
+				margin-right 8px
+
+	> .more
+		display block
+		width 100%
+		padding 16px
+		color #555
+		border-top solid 1px rgba(0, 0, 0, 0.05)
+
+		&:hover
+			background rgba(0, 0, 0, 0.025)
+
+		&:active
+			background rgba(0, 0, 0, 0.05)
+
+		&.fetching
+			cursor wait
+
+		> [data-fa]
+			margin-right 4px
+
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+	> .loading
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+</style>

From 6f9684cb3f820142f39431e0d6ff0369ad449871 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 21:01:39 +0900
Subject: [PATCH 090/286] wip

---
 .../desktop/views/components/ui-header.vue    | 22 +++++++++----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/src/web/app/desktop/views/components/ui-header.vue b/src/web/app/desktop/views/components/ui-header.vue
index 19e4fe697..0d9ecc4a5 100644
--- a/src/web/app/desktop/views/components/ui-header.vue
+++ b/src/web/app/desktop/views/components/ui-header.vue
@@ -62,25 +62,25 @@
 			user-select none
 
 			> .container
+				display flex
 				width 100%
 				max-width 1300px
 				margin 0 auto
 
-				&:after
-					content ""
-					display block
-					clear both
-
 				> .left
-					float left
-					height 3rem
-
-				> .right
-					float right
+					margin 0 auto 0 0
 					height 48px
 
+				> .right
+					margin 0 0 0 auto
+					height 48px
+
+					> *
+						display inline-block
+						vertical-align top
+
 					@media (max-width 1100px)
-						> mk-ui-header-search
+						> .mk-ui-header-search
 							display none
 
 </style>

From 284b21922c252c1719e94f7527abcdbd6f5c05d7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Feb 2018 21:04:08 +0900
Subject: [PATCH 091/286] wip

---
 src/web/app/desktop/views/components/index.ts | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index f212338e1..580c61592 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -23,6 +23,9 @@ import ellipsisIcon from './ellipsis-icon.vue';
 import images from './images.vue';
 import imagesImage from './images-image.vue';
 import imagesImageDialog from './images-image-dialog.vue';
+import notifications from './notifications.vue';
+import postForm from './post-form.vue';
+import repostForm from './repost-form.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-header', uiHeader);
@@ -47,3 +50,6 @@ Vue.component('mk-ellipsis-icon', ellipsisIcon);
 Vue.component('mk-images', images);
 Vue.component('mk-images-image', imagesImage);
 Vue.component('mk-images-image-dialog', imagesImageDialog);
+Vue.component('mk-notifications', notifications);
+Vue.component('mk-post-form', postForm);
+Vue.component('mk-repost-form', repostForm);

From 72e09b86b616cf2272efc741e951d253b49ab896 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 12:24:49 +0900
Subject: [PATCH 092/286] wip

---
 src/web/app/mobile/tags/home-timeline.tag |  69 -----------
 src/web/app/mobile/tags/timeline.tag      | 137 ----------------------
 src/web/app/mobile/views/posts.vue        |  97 +++++++++++++++
 src/web/app/mobile/views/timeline.vue     |  89 ++++++++++++++
 4 files changed, 186 insertions(+), 206 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/home-timeline.tag
 create mode 100644 src/web/app/mobile/views/posts.vue
 create mode 100644 src/web/app/mobile/views/timeline.vue

diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag
deleted file mode 100644
index 88e26bc78..000000000
--- a/src/web/app/mobile/tags/home-timeline.tag
+++ /dev/null
@@ -1,69 +0,0 @@
-<mk-home-timeline>
-	<mk-init-following v-if="noFollowing" />
-	<mk-timeline ref="timeline" init={ init } more={ more } empty={ '%i18n:mobile.tags.mk-home-timeline.empty-timeline%' }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> mk-init-following
-				margin-bottom 8px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-		this.mixin('api');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.noFollowing = this.I.following_count == 0;
-
-		this.init = new Promise((res, rej) => {
-			this.api('posts/timeline').then(posts => {
-				res(posts);
-				this.$emit('loaded');
-			});
-		});
-
-		this.fetch = () => {
-			this.api('posts/timeline').then(posts => {
-				this.$refs.timeline.setPosts(posts);
-			});
-		};
-
-		this.on('mount', () => {
-			this.connection.on('post', this.onStreamPost);
-			this.connection.on('follow', this.onStreamFollow);
-			this.connection.on('unfollow', this.onStreamUnfollow);
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('post', this.onStreamPost);
-			this.connection.off('follow', this.onStreamFollow);
-			this.connection.off('unfollow', this.onStreamUnfollow);
-			this.stream.dispose(this.connectionId);
-		});
-
-		this.more = () => {
-			return this.api('posts/timeline', {
-				until_id: this.$refs.timeline.tail().id
-			});
-		};
-
-		this.onStreamPost = post => {
-			this.update({
-				isEmpty: false
-			});
-			this.$refs.timeline.addPost(post);
-		};
-
-		this.onStreamFollow = () => {
-			this.fetch();
-		};
-
-		this.onStreamUnfollow = () => {
-			this.fetch();
-		};
-	</script>
-</mk-home-timeline>
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index ed3f88c04..8a4d72b67 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -1,140 +1,3 @@
-<mk-timeline>
-	<div class="init" v-if="init">
-		%fa:spinner .pulse%%i18n:common.loading%
-	</div>
-	<div class="empty" v-if="!init && posts.length == 0">
-		%fa:R comments%{ opts.empty || '%i18n:mobile.tags.mk-timeline.empty%' }
-	</div>
-	<template each={ post, i in posts }>
-		<mk-timeline-post post={ post }/>
-		<p class="date" v-if="i != posts.length - 1 && post._date != posts[i + 1]._date">
-			<span>%fa:angle-up%{ post._datetext }</span>
-			<span>%fa:angle-down%{ posts[i + 1]._datetext }</span>
-		</p>
-	</template>
-	<footer v-if="!init">
-		<button v-if="canFetchMore" @click="more" disabled={ fetching }>
-			<span v-if="!fetching">%i18n:mobile.tags.mk-timeline.load-more%</span>
-			<span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span>
-		</button>
-	</footer>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border-radius 8px
-			box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
-
-			> .init
-				padding 64px 0
-				text-align center
-				color #999
-
-				> [data-fa]
-					margin-right 4px
-
-			> .empty
-				margin 0 auto
-				padding 32px
-				max-width 400px
-				text-align center
-				color #999
-
-				> [data-fa]
-					display block
-					margin-bottom 16px
-					font-size 3em
-					color #ccc
-
-			> .date
-				display block
-				margin 0
-				line-height 32px
-				text-align center
-				font-size 0.9em
-				color #aaa
-				background #fdfdfd
-				border-bottom solid 1px #eaeaea
-
-				span
-					margin 0 16px
-
-				[data-fa]
-					margin-right 8px
-
-			> footer
-				text-align center
-				border-top solid 1px #eaeaea
-				border-bottom-left-radius 4px
-				border-bottom-right-radius 4px
-
-				> button
-					margin 0
-					padding 16px
-					width 100%
-					color $theme-color
-					border-radius 0 0 8px 8px
-
-					&:disabled
-						opacity 0.7
-
-	</style>
-	<script lang="typescript">
-		this.posts = [];
-		this.init = true;
-		this.fetching = false;
-		this.canFetchMore = true;
-
-		this.on('mount', () => {
-			this.opts.init.then(posts => {
-				this.init = false;
-				this.setPosts(posts);
-			});
-		});
-
-		this.on('update', () => {
-			this.posts.forEach(post => {
-				const date = new Date(post.created_at).getDate();
-				const month = new Date(post.created_at).getMonth() + 1;
-				post._date = date;
-				post._datetext = `${month}月 ${date}日`;
-			});
-		});
-
-		this.more = () => {
-			if (this.init || this.fetching || this.posts.length == 0) return;
-			this.update({
-				fetching: true
-			});
-			this.opts.more().then(posts => {
-				this.fetching = false;
-				this.prependPosts(posts);
-			});
-		};
-
-		this.setPosts = posts => {
-			this.update({
-				posts: posts
-			});
-		};
-
-		this.prependPosts = posts => {
-			posts.forEach(post => {
-				this.posts.push(post);
-				this.update();
-			});
-		}
-
-		this.addPost = post => {
-			this.posts.unshift(post);
-			this.update();
-		};
-
-		this.tail = () => {
-			return this.posts[this.posts.length - 1];
-		};
-	</script>
-</mk-timeline>
 
 <mk-timeline-post :class="{ repost: isRepost }">
 	<div class="reply-to" v-if="p.reply">
diff --git a/src/web/app/mobile/views/posts.vue b/src/web/app/mobile/views/posts.vue
new file mode 100644
index 000000000..0edda5e94
--- /dev/null
+++ b/src/web/app/mobile/views/posts.vue
@@ -0,0 +1,97 @@
+<template>
+<div class="mk-posts">
+	<slot name="head"></slot>
+	<template v-for="(post, i) in _posts">
+		<mk-posts-post :post="post" :key="post.id"/>
+		<p class="date" :key="post._datetext" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date">
+			<span>%fa:angle-up%{{ post._datetext }}</span>
+			<span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span>
+		</p>
+	</template>
+	<slot name="tail"></slot>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		posts: {
+			type: Array,
+			default: () => []
+		}
+	},
+	computed: {
+		_posts(): any[] {
+			return (this.posts as any).map(post => {
+				const date = new Date(post.created_at).getDate();
+				const month = new Date(post.created_at).getMonth() + 1;
+				post._date = date;
+				post._datetext = `${month}月 ${date}日`;
+				return post;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-posts
+	background #fff
+	border-radius 8px
+	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+
+	> .init
+		padding 64px 0
+		text-align center
+		color #999
+
+		> [data-fa]
+			margin-right 4px
+
+	> .empty
+		margin 0 auto
+		padding 32px
+		max-width 400px
+		text-align center
+		color #999
+
+		> [data-fa]
+			display block
+			margin-bottom 16px
+			font-size 3em
+			color #ccc
+
+	> .date
+		display block
+		margin 0
+		line-height 32px
+		text-align center
+		font-size 0.9em
+		color #aaa
+		background #fdfdfd
+		border-bottom solid 1px #eaeaea
+
+		span
+			margin 0 16px
+
+		[data-fa]
+			margin-right 8px
+
+	> footer
+		text-align center
+		border-top solid 1px #eaeaea
+		border-bottom-left-radius 4px
+		border-bottom-right-radius 4px
+
+		> button
+			margin 0
+			padding 16px
+			width 100%
+			color $theme-color
+			border-radius 0 0 8px 8px
+
+			&:disabled
+				opacity 0.7
+
+</style>
diff --git a/src/web/app/mobile/views/timeline.vue b/src/web/app/mobile/views/timeline.vue
new file mode 100644
index 000000000..3a5df7792
--- /dev/null
+++ b/src/web/app/mobile/views/timeline.vue
@@ -0,0 +1,89 @@
+<template>
+<div class="mk-timeline">
+	<mk-posts ref="timeline" :posts="posts">
+		<mk-friends-maker v-if="alone" slot="head"/>
+		<div class="init" v-if="fetching">
+			%fa:spinner .pulse%%i18n:common.loading%
+		</div>
+		<div class="empty" v-if="!fetching && posts.length == 0">
+			%fa:R comments%
+			%i18n:mobile.tags.mk-home-timeline.empty-timeline%
+		</div>
+		<button v-if="canFetchMore" @click="more" :disabled="fetching" slot="tail">
+			<span v-if="!fetching">%i18n:mobile.tags.mk-timeline.load-more%</span>
+			<span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span>
+		</button>
+	</mk-posts>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		date: {
+			type: Date,
+			required: false
+		}
+	},
+	data() {
+		return {
+			fetching: true,
+			moreFetching: false,
+			posts: [],
+			connection: null,
+			connectionId: null
+		};
+	},
+	computed: {
+		alone(): boolean {
+			return this.$root.$data.os.i.following_count == 0;
+		}
+	},
+	mounted() {
+		this.connection = this.$root.$data.os.stream.getConnection();
+		this.connectionId = this.$root.$data.os.stream.use();
+
+		this.connection.on('post', this.onPost);
+		this.connection.on('follow', this.onChangeFollowing);
+		this.connection.on('unfollow', this.onChangeFollowing);
+
+		this.fetch();
+	},
+	beforeDestroy() {
+		this.connection.off('post', this.onPost);
+		this.connection.off('follow', this.onChangeFollowing);
+		this.connection.off('unfollow', this.onChangeFollowing);
+		this.$root.$data.os.stream.dispose(this.connectionId);
+	},
+	methods: {
+		fetch(cb?) {
+			this.fetching = true;
+
+			this.$root.$data.os.api('posts/timeline', {
+				until_date: this.date ? (this.date as any).getTime() : undefined
+			}).then(posts => {
+				this.fetching = false;
+				this.posts = posts;
+				if (cb) cb();
+			});
+		},
+		more() {
+			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
+			this.moreFetching = true;
+			this.$root.$data.os.api('posts/timeline', {
+				until_id: this.posts[this.posts.length - 1].id
+			}).then(posts => {
+				this.moreFetching = false;
+				this.posts.unshift(posts);
+			});
+		},
+		onPost(post) {
+			this.posts.unshift(post);
+		},
+		onChangeFollowing() {
+			this.fetch();
+		}
+	}
+});
+</script>

From 80f69543db076f3e6098e9aefdbde98c8ffedcf4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 13:11:20 +0900
Subject: [PATCH 093/286] wip

---
 src/web/app/mobile/tags/timeline.tag        | 551 --------------------
 src/web/app/mobile/views/posts-post-sub.vue | 117 +++++
 src/web/app/mobile/views/posts-post.vue     | 412 +++++++++++++++
 3 files changed, 529 insertions(+), 551 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/timeline.tag
 create mode 100644 src/web/app/mobile/views/posts-post-sub.vue
 create mode 100644 src/web/app/mobile/views/posts-post.vue

diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
deleted file mode 100644
index 8a4d72b67..000000000
--- a/src/web/app/mobile/tags/timeline.tag
+++ /dev/null
@@ -1,551 +0,0 @@
-
-<mk-timeline-post :class="{ repost: isRepost }">
-	<div class="reply-to" v-if="p.reply">
-		<mk-timeline-post-sub post={ p.reply }/>
-	</div>
-	<div class="repost" v-if="isRepost">
-		<p>
-			<a class="avatar-anchor" href={ '/' + post.user.username }>
-				<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-			</a>
-			%fa:retweet%{'%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{'))}<a class="name" href={ '/' + post.user.username }>{ post.user.name }</a>{'%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1)}
-		</p>
-		<mk-time time={ post.created_at }/>
-	</div>
-	<article>
-		<a class="avatar-anchor" href={ '/' + p.user.username }>
-			<img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=96' } alt="avatar"/>
-		</a>
-		<div class="main">
-			<header>
-				<a class="name" href={ '/' + p.user.username }>{ p.user.name }</a>
-				<span class="is-bot" v-if="p.user.is_bot">bot</span>
-				<span class="username">@{ p.user.username }</span>
-				<a class="created-at" href={ url }>
-					<mk-time time={ p.created_at }/>
-				</a>
-			</header>
-			<div class="body">
-				<div class="text" ref="text">
-					<p class="channel" v-if="p.channel != null"><a href={ _CH_URL_ + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
-					<a class="reply" v-if="p.reply">
-						%fa:reply%
-					</a>
-					<p class="dummy"></p>
-					<a class="quote" v-if="p.repost != null">RP:</a>
-				</div>
-				<div class="media" v-if="p.media">
-					<mk-images images={ p.media }/>
-				</div>
-				<mk-poll v-if="p.poll" post={ p } ref="pollViewer"/>
-				<span class="app" v-if="p.app">via <b>{ p.app.name }</b></span>
-				<div class="repost" v-if="p.repost">%fa:quote-right -flip-h%
-					<mk-post-preview class="repost" post={ p.repost }/>
-				</div>
-			</div>
-			<footer>
-				<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
-				<button @click="reply">
-					%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
-				</button>
-				<button @click="repost" title="Repost">
-					%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
-				</button>
-				<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton">
-					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
-				</button>
-				<button class="menu" @click="menu" ref="menuButton">
-					%fa:ellipsis-h%
-				</button>
-			</footer>
-		</div>
-	</article>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 0
-			font-size 12px
-			border-bottom solid 1px #eaeaea
-
-			&:first-child
-				border-radius 8px 8px 0 0
-
-				> .repost
-					border-radius 8px 8px 0 0
-
-			&:last-of-type
-				border-bottom none
-
-			@media (min-width 350px)
-				font-size 14px
-
-			@media (min-width 500px)
-				font-size 16px
-
-			> .repost
-				color #9dbb00
-				background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
-
-				> p
-					margin 0
-					padding 8px 16px
-					line-height 28px
-
-					@media (min-width 500px)
-						padding 16px
-
-					.avatar-anchor
-						display inline-block
-
-						.avatar
-							vertical-align bottom
-							width 28px
-							height 28px
-							margin 0 8px 0 0
-							border-radius 6px
-
-					[data-fa]
-						margin-right 4px
-
-					.name
-						font-weight bold
-
-				> mk-time
-					position absolute
-					top 8px
-					right 16px
-					font-size 0.9em
-					line-height 28px
-
-					@media (min-width 500px)
-						top 16px
-
-				& + article
-					padding-top 8px
-
-			> .reply-to
-				background rgba(0, 0, 0, 0.0125)
-
-				> mk-post-preview
-					background transparent
-
-			> article
-				padding 14px 16px 9px 16px
-
-				&:after
-					content ""
-					display block
-					clear both
-
-				> .avatar-anchor
-					display block
-					float left
-					margin 0 10px 8px 0
-					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
-
-				> .main
-					float left
-					width calc(100% - 58px)
-
-					@media (min-width 500px)
-						width calc(100% - 74px)
-
-					> header
-						display flex
-						white-space nowrap
-
-						@media (min-width 500px)
-							margin-bottom 2px
-
-						> .name
-							display block
-							margin 0 0.5em 0 0
-							padding 0
-							overflow hidden
-							color #777
-							font-size 1em
-							font-weight 700
-							text-align left
-							text-decoration none
-							text-overflow ellipsis
-
-							&:hover
-								text-decoration underline
-
-						> .is-bot
-							text-align left
-							margin 0 0.5em 0 0
-							padding 1px 6px
-							font-size 12px
-							color #aaa
-							border solid 1px #ddd
-							border-radius 3px
-
-						> .username
-							text-align left
-							margin 0 0.5em 0 0
-							color #ccc
-
-						> .created-at
-							margin-left auto
-							font-size 0.9em
-							color #c0c0c0
-
-					> .body
-
-						> .text
-							cursor default
-							display block
-							margin 0
-							padding 0
-							overflow-wrap break-word
-							font-size 1.1em
-							color #717171
-
-							> .dummy
-								display none
-
-							mk-url-preview
-								margin-top 8px
-
-							> .channel
-								margin 0
-
-							> .reply
-								margin-right 8px
-								color #717171
-
-							> .quote
-								margin-left 4px
-								font-style oblique
-								color #a0bf46
-
-							code
-								padding 4px 8px
-								margin 0 0.5em
-								font-size 80%
-								color #525252
-								background #f8f8f8
-								border-radius 2px
-
-							pre > code
-								padding 16px
-								margin 0
-
-							[data-is-me]:after
-								content "you"
-								padding 0 4px
-								margin-left 4px
-								font-size 80%
-								color $theme-color-foreground
-								background $theme-color
-								border-radius 4px
-
-						> .media
-							> img
-								display block
-								max-width 100%
-
-						> .app
-							font-size 12px
-							color #ccc
-
-						> mk-poll
-							font-size 80%
-
-						> .repost
-							margin 8px 0
-
-							> [data-fa]:first-child
-								position absolute
-								top -8px
-								left -8px
-								z-index 1
-								color #c0dac6
-								font-size 28px
-								background #fff
-
-							> mk-post-preview
-								padding 16px
-								border dashed 1px #c0dac6
-								border-radius 8px
-
-					> footer
-						> button
-							margin 0
-							padding 8px
-							background transparent
-							border none
-							box-shadow none
-							font-size 1em
-							color #ddd
-							cursor pointer
-
-							&:not(:last-child)
-								margin-right 28px
-
-							&:hover
-								color #666
-
-							> .count
-								display inline
-								margin 0 0 0 8px
-								color #999
-
-							&.reacted
-								color $theme-color
-
-							&.menu
-								@media (max-width 350px)
-									display none
-
-	</style>
-	<script lang="typescript">
-		import compile from '../../common/scripts/text-compiler';
-		import getPostSummary from '../../../../common/get-post-summary.ts';
-		import openPostForm from '../scripts/open-post-form';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.set = post => {
-			this.post = post;
-			this.isRepost = this.post.repost != null && this.post.text == null;
-			this.p = this.isRepost ? this.post.repost : this.post;
-			this.p.reactions_count = this.p.reaction_counts ? Object.keys(this.p.reaction_counts).map(key => this.p.reaction_counts[key]).reduce((a, b) => a + b) : 0;
-			this.summary = getPostSummary(this.p);
-			this.url = `/${this.p.user.username}/${this.p.id}`;
-		};
-
-		this.set(this.opts.post);
-
-		this.refresh = post => {
-			this.set(post);
-			this.update();
-			if (this.$refs.reactionsViewer) this.$refs.reactionsViewer.update({
-				post
-			});
-			if (this.$refs.pollViewer) this.$refs.pollViewer.init(post);
-		};
-
-		this.onStreamPostUpdated = data => {
-			const post = data.post;
-			if (post.id == this.post.id) {
-				this.refresh(post);
-			}
-		};
-
-		this.onStreamConnected = () => {
-			this.capture();
-		};
-
-		this.capture = withHandler => {
-			if (this.SIGNIN) {
-				this.connection.send({
-					type: 'capture',
-					id: this.post.id
-				});
-				if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated);
-			}
-		};
-
-		this.decapture = withHandler => {
-			if (this.SIGNIN) {
-				this.connection.send({
-					type: 'decapture',
-					id: this.post.id
-				});
-				if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated);
-			}
-		};
-
-		this.on('mount', () => {
-			this.capture(true);
-
-			if (this.SIGNIN) {
-				this.connection.on('_connected_', this.onStreamConnected);
-			}
-
-			if (this.p.text) {
-				const tokens = this.p.ast;
-
-				this.$refs.text.innerHTML = this.$refs.text.innerHTML.replace('<p class="dummy"></p>', compile(tokens));
-
-				Array.from(this.$refs.text.children).forEach(e => {
-					if (e.tagName == 'MK-URL') riot.mount(e);
-				});
-
-				// URLをプレビュー
-				tokens
-				.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
-				.map(t => {
-					riot.mount(this.$refs.text.appendChild(document.createElement('mk-url-preview')), {
-						url: t.url
-					});
-				});
-			}
-		});
-
-		this.on('unmount', () => {
-			this.decapture(true);
-			this.connection.off('_connected_', this.onStreamConnected);
-			this.stream.dispose(this.connectionId);
-		});
-
-		this.reply = () => {
-			openPostForm({
-				reply: this.p
-			});
-		};
-
-		this.repost = () => {
-			const text = window.prompt(`「${this.summary}」をRepost`);
-			if (text == null) return;
-			this.api('posts/create', {
-				repost_id: this.p.id,
-				text: text == '' ? undefined : text
-			});
-		};
-
-		this.react = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), {
-				source: this.$refs.reactButton,
-				post: this.p,
-				compact: true
-			});
-		};
-
-		this.menu = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
-				source: this.$refs.menuButton,
-				post: this.p,
-				compact: true
-			});
-		};
-	</script>
-</mk-timeline-post>
-
-<mk-timeline-post-sub>
-	<article><a class="avatar-anchor" href={ '/' + post.user.username }><img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=96' } alt="avatar"/></a>
-		<div class="main">
-			<header><a class="name" href={ '/' + post.user.username }>{ post.user.name }</a><span class="username">@{ post.user.username }</span><a class="created-at" href={ '/' + post.user.username + '/' + post.id }>
-					<mk-time time={ post.created_at }/></a></header>
-			<div class="body">
-				<mk-sub-post-content class="text" post={ post }/>
-			</div>
-		</div>
-	</article>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 0
-			font-size 0.9em
-
-			> article
-				padding 16px
-
-				&:after
-					content ""
-					display block
-					clear both
-
-				&:hover
-					> .main > footer > button
-						color #888
-
-				> .avatar-anchor
-					display block
-					float left
-					margin 0 10px 0 0
-
-					@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
-
-				> .main
-					float left
-					width calc(100% - 54px)
-
-					@media (min-width 500px)
-						width calc(100% - 68px)
-
-					> header
-						display flex
-						margin-bottom 2px
-						white-space nowrap
-
-						> .name
-							display block
-							margin 0 0.5em 0 0
-							padding 0
-							overflow hidden
-							color #607073
-							font-size 1em
-							font-weight 700
-							text-align left
-							text-decoration none
-							text-overflow ellipsis
-
-							&:hover
-								text-decoration underline
-
-						> .username
-							text-align left
-							margin 0
-							color #d1d8da
-
-						> .created-at
-							margin-left auto
-							color #b2b8bb
-
-					> .body
-
-						> .text
-							cursor default
-							margin 0
-							padding 0
-							font-size 1.1em
-							color #717171
-
-							pre
-								max-height 120px
-								font-size 80%
-
-	</style>
-	<script lang="typescript">this.post = this.opts.post</script>
-</mk-timeline-post-sub>
diff --git a/src/web/app/mobile/views/posts-post-sub.vue b/src/web/app/mobile/views/posts-post-sub.vue
new file mode 100644
index 000000000..421d51b92
--- /dev/null
+++ b/src/web/app/mobile/views/posts-post-sub.vue
@@ -0,0 +1,117 @@
+<template>
+<div class="mk-posts-post-sub">
+	<article>
+		<a class="avatar-anchor" href={ '/' + post.user.username }>
+			<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=96' } alt="avatar"/>
+		</a>
+		<div class="main">
+			<header>
+				<a class="name" href={ '/' + post.user.username }>{ post.user.name }</a>
+				<span class="username">@{ post.user.username }</span>
+				<a class="created-at" href={ '/' + post.user.username + '/' + post.id }>
+					<mk-time time={ post.created_at }/>
+				</a>
+			</header>
+			<div class="body">
+				<mk-sub-post-content class="text" post={ post }/>
+			</div>
+		</div>
+	</article>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['post']
+});
+</script>
+
+
+<style lang="stylus" scoped>
+.mk-posts-post-sub
+	font-size 0.9em
+
+	> article
+		padding 16px
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		&:hover
+			> .main > footer > button
+				color #888
+
+		> .avatar-anchor
+			display block
+			float left
+			margin 0 10px 0 0
+
+			@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
+
+		> .main
+			float left
+			width calc(100% - 54px)
+
+			@media (min-width 500px)
+				width calc(100% - 68px)
+
+			> header
+				display flex
+				margin-bottom 2px
+				white-space nowrap
+
+				> .name
+					display block
+					margin 0 0.5em 0 0
+					padding 0
+					overflow hidden
+					color #607073
+					font-size 1em
+					font-weight 700
+					text-align left
+					text-decoration none
+					text-overflow ellipsis
+
+					&:hover
+						text-decoration underline
+
+				> .username
+					text-align left
+					margin 0
+					color #d1d8da
+
+				> .created-at
+					margin-left auto
+					color #b2b8bb
+
+			> .body
+
+				> .text
+					cursor default
+					margin 0
+					padding 0
+					font-size 1.1em
+					color #717171
+
+					pre
+						max-height 120px
+						font-size 80%
+
+</style>
+
diff --git a/src/web/app/mobile/views/posts-post.vue b/src/web/app/mobile/views/posts-post.vue
new file mode 100644
index 000000000..4dd82e648
--- /dev/null
+++ b/src/web/app/mobile/views/posts-post.vue
@@ -0,0 +1,412 @@
+<template>
+<div class="mk-posts-post" :class="{ repost: isRepost }">
+	<div class="reply-to" v-if="p.reply">
+		<mk-timeline-post-sub post={ p.reply }/>
+	</div>
+	<div class="repost" v-if="isRepost">
+		<p>
+			<a class="avatar-anchor" href={ '/' + post.user.username }>
+				<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
+			</a>
+			%fa:retweet%{'%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{'))}<a class="name" href={ '/' + post.user.username }>{ post.user.name }</a>{'%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1)}
+		</p>
+		<mk-time time={ post.created_at }/>
+	</div>
+	<article>
+		<a class="avatar-anchor" href={ '/' + p.user.username }>
+			<img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=96' } alt="avatar"/>
+		</a>
+		<div class="main">
+			<header>
+				<a class="name" href={ '/' + p.user.username }>{ p.user.name }</a>
+				<span class="is-bot" v-if="p.user.is_bot">bot</span>
+				<span class="username">@{ p.user.username }</span>
+				<a class="created-at" href={ url }>
+					<mk-time time={ p.created_at }/>
+				</a>
+			</header>
+			<div class="body">
+				<div class="text" ref="text">
+					<p class="channel" v-if="p.channel != null"><a href={ _CH_URL_ + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
+					<a class="reply" v-if="p.reply">
+						%fa:reply%
+					</a>
+					<p class="dummy"></p>
+					<a class="quote" v-if="p.repost != null">RP:</a>
+				</div>
+				<div class="media" v-if="p.media">
+					<mk-images images={ p.media }/>
+				</div>
+				<mk-poll v-if="p.poll" post={ p } ref="pollViewer"/>
+				<span class="app" v-if="p.app">via <b>{ p.app.name }</b></span>
+				<div class="repost" v-if="p.repost">%fa:quote-right -flip-h%
+					<mk-post-preview class="repost" post={ p.repost }/>
+				</div>
+			</div>
+			<footer>
+				<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
+				<button @click="reply">
+					%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
+				</button>
+				<button @click="repost" title="Repost">
+					%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
+				</button>
+				<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton">
+					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
+				</button>
+				<button class="menu" @click="menu" ref="menuButton">
+					%fa:ellipsis-h%
+				</button>
+			</footer>
+		</div>
+	</article>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import openPostForm from '../scripts/open-post-form';
+
+export default Vue.extend({
+	props: ['post'],
+	data() {
+		return {
+			connection: null,
+			connectionId: null
+		};
+	},
+	computed: {
+		isRepost(): boolean {
+			return (this.post.repost &&
+				this.post.text == null &&
+				this.post.media_ids == null &&
+				this.post.poll == null);
+		},
+		p(): any {
+			return this.isRepost ? this.post.repost : this.post;
+		},
+		reactionsCount(): number {
+			return this.p.reaction_counts
+				? Object.keys(this.p.reaction_counts)
+					.map(key => this.p.reaction_counts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		url(): string {
+			return `/${this.p.user.username}/${this.p.id}`;
+		},
+		urls(): string[] {
+			if (this.p.ast) {
+				return this.p.ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
+		}
+	},
+	created() {
+		this.connection = this.$root.$data.os.stream.getConnection();
+		this.connectionId = this.$root.$data.os.stream.use();
+	},
+	mounted() {
+		this.capture(true);
+
+		if (this.$root.$data.os.isSignedIn) {
+			this.connection.on('_connected_', this.onStreamConnected);
+		}
+	},
+	beforeDestroy() {
+		this.decapture(true);
+		this.connection.off('_connected_', this.onStreamConnected);
+		this.$root.$data.os.stream.dispose(this.connectionId);
+	},
+	methods: {
+		capture(withHandler = false) {
+			if (this.$root.$data.os.isSignedIn) {
+				this.connection.send({
+					type: 'capture',
+					id: this.post.id
+				});
+				if (withHandler) this.connection.on('post-updated', this.onStreamPostUpdated);
+			}
+		},
+		decapture(withHandler = false) {
+			if (this.$root.$data.os.isSignedIn) {
+				this.connection.send({
+					type: 'decapture',
+					id: this.post.id
+				});
+				if (withHandler) this.connection.off('post-updated', this.onStreamPostUpdated);
+			}
+		},
+		onStreamConnected() {
+			this.capture();
+		},
+		onStreamPostUpdated(data) {
+			const post = data.post;
+			if (post.id == this.post.id) {
+				this.$emit('update:post', post);
+			}
+		},
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-posts-post
+	font-size 12px
+	border-bottom solid 1px #eaeaea
+
+	&:first-child
+		border-radius 8px 8px 0 0
+
+		> .repost
+			border-radius 8px 8px 0 0
+
+	&:last-of-type
+		border-bottom none
+
+	@media (min-width 350px)
+		font-size 14px
+
+	@media (min-width 500px)
+		font-size 16px
+
+	> .repost
+		color #9dbb00
+		background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+
+		> p
+			margin 0
+			padding 8px 16px
+			line-height 28px
+
+			@media (min-width 500px)
+				padding 16px
+
+			.avatar-anchor
+				display inline-block
+
+				.avatar
+					vertical-align bottom
+					width 28px
+					height 28px
+					margin 0 8px 0 0
+					border-radius 6px
+
+			[data-fa]
+				margin-right 4px
+
+			.name
+				font-weight bold
+
+		> mk-time
+			position absolute
+			top 8px
+			right 16px
+			font-size 0.9em
+			line-height 28px
+
+			@media (min-width 500px)
+				top 16px
+
+		& + article
+			padding-top 8px
+
+	> .reply-to
+		background rgba(0, 0, 0, 0.0125)
+
+		> mk-post-preview
+			background transparent
+
+	> article
+		padding 14px 16px 9px 16px
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		> .avatar-anchor
+			display block
+			float left
+			margin 0 10px 8px 0
+			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
+
+		> .main
+			float left
+			width calc(100% - 58px)
+
+			@media (min-width 500px)
+				width calc(100% - 74px)
+
+			> header
+				display flex
+				white-space nowrap
+
+				@media (min-width 500px)
+					margin-bottom 2px
+
+				> .name
+					display block
+					margin 0 0.5em 0 0
+					padding 0
+					overflow hidden
+					color #777
+					font-size 1em
+					font-weight 700
+					text-align left
+					text-decoration none
+					text-overflow ellipsis
+
+					&:hover
+						text-decoration underline
+
+				> .is-bot
+					text-align left
+					margin 0 0.5em 0 0
+					padding 1px 6px
+					font-size 12px
+					color #aaa
+					border solid 1px #ddd
+					border-radius 3px
+
+				> .username
+					text-align left
+					margin 0 0.5em 0 0
+					color #ccc
+
+				> .created-at
+					margin-left auto
+					font-size 0.9em
+					color #c0c0c0
+
+			> .body
+
+				> .text
+					cursor default
+					display block
+					margin 0
+					padding 0
+					overflow-wrap break-word
+					font-size 1.1em
+					color #717171
+
+					> .dummy
+						display none
+
+					mk-url-preview
+						margin-top 8px
+
+					> .channel
+						margin 0
+
+					> .reply
+						margin-right 8px
+						color #717171
+
+					> .quote
+						margin-left 4px
+						font-style oblique
+						color #a0bf46
+
+					code
+						padding 4px 8px
+						margin 0 0.5em
+						font-size 80%
+						color #525252
+						background #f8f8f8
+						border-radius 2px
+
+					pre > code
+						padding 16px
+						margin 0
+
+					[data-is-me]:after
+						content "you"
+						padding 0 4px
+						margin-left 4px
+						font-size 80%
+						color $theme-color-foreground
+						background $theme-color
+						border-radius 4px
+
+				> .media
+					> img
+						display block
+						max-width 100%
+
+				> .app
+					font-size 12px
+					color #ccc
+
+				> mk-poll
+					font-size 80%
+
+				> .repost
+					margin 8px 0
+
+					> [data-fa]:first-child
+						position absolute
+						top -8px
+						left -8px
+						z-index 1
+						color #c0dac6
+						font-size 28px
+						background #fff
+
+					> mk-post-preview
+						padding 16px
+						border dashed 1px #c0dac6
+						border-radius 8px
+
+			> footer
+				> button
+					margin 0
+					padding 8px
+					background transparent
+					border none
+					box-shadow none
+					font-size 1em
+					color #ddd
+					cursor pointer
+
+					&:not(:last-child)
+						margin-right 28px
+
+					&:hover
+						color #666
+
+					> .count
+						display inline
+						margin 0 0 0 8px
+						color #999
+
+					&.reacted
+						color $theme-color
+
+					&.menu
+						@media (max-width 350px)
+							display none
+
+</style>
+

From aafc5847560d56dd4fa0997d65f779aece04c643 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 13:26:08 +0900
Subject: [PATCH 094/286] wip

---
 src/web/app/mobile/tags/post-form.tag  | 275 -------------------------
 src/web/app/mobile/views/post-form.vue | 204 ++++++++++++++++++
 2 files changed, 204 insertions(+), 275 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/post-form.tag
 create mode 100644 src/web/app/mobile/views/post-form.vue

diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
deleted file mode 100644
index a37e2bf38..000000000
--- a/src/web/app/mobile/tags/post-form.tag
+++ /dev/null
@@ -1,275 +0,0 @@
-<mk-post-form>
-	<header>
-		<button class="cancel" @click="cancel">%fa:times%</button>
-		<div>
-			<span v-if="refs.text" class="text-count { over: refs.text.value.length > 1000 }">{ 1000 - refs.text.value.length }</span>
-			<button class="submit" @click="post">%i18n:mobile.tags.mk-post-form.submit%</button>
-		</div>
-	</header>
-	<div class="form">
-		<mk-post-preview v-if="opts.reply" post={ opts.reply }/>
-		<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ opts.reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.post-placeholder%' }></textarea>
-		<div class="attaches" show={ files.length != 0 }>
-			<ul class="files" ref="attaches">
-				<li class="file" each={ files } data-id={ id }>
-					<div class="img" style="background-image: url({ url + '?thumbnail&size=128' })" @click="removeFile"></div>
-				</li>
-			</ul>
-		</div>
-		<mk-poll-editor v-if="poll" ref="poll" ondestroy={ onPollDestroyed }/>
-		<mk-uploader ref="uploader"/>
-		<button ref="upload" @click="selectFile">%fa:upload%</button>
-		<button ref="drive" @click="selectFileFromDrive">%fa:cloud%</button>
-		<button class="kao" @click="kao">%fa:R smile%</button>
-		<button class="poll" @click="addPoll">%fa:chart-pie%</button>
-		<input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			max-width 500px
-			width calc(100% - 16px)
-			margin 8px auto
-			background #fff
-			border-radius 8px
-			box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
-
-			@media (min-width 500px)
-				margin 16px auto
-				width calc(100% - 32px)
-
-			> header
-				z-index 1
-				height 50px
-				box-shadow 0 1px 0 0 rgba(0, 0, 0, 0.1)
-
-				> .cancel
-					width 50px
-					line-height 50px
-					font-size 24px
-					color #555
-
-				> div
-					position absolute
-					top 0
-					right 0
-
-					> .text-count
-						line-height 50px
-						color #657786
-
-					> .submit
-						margin 8px
-						padding 0 16px
-						line-height 34px
-						color $theme-color-foreground
-						background $theme-color
-						border-radius 4px
-
-						&:disabled
-							opacity 0.7
-
-			> .form
-				max-width 500px
-				margin 0 auto
-
-				> mk-post-preview
-					padding 16px
-
-				> .attaches
-
-					> .files
-						display block
-						margin 0
-						padding 4px
-						list-style none
-
-						&:after
-							content ""
-							display block
-							clear both
-
-						> .file
-							display block
-							float left
-							margin 0
-							padding 0
-							border solid 4px transparent
-
-							> .img
-								width 64px
-								height 64px
-								background-size cover
-								background-position center center
-
-				> mk-uploader
-					margin 8px 0 0 0
-					padding 8px
-
-				> [ref='file']
-					display none
-
-				> [ref='text']
-					display block
-					padding 12px
-					margin 0
-					width 100%
-					max-width 100%
-					min-width 100%
-					min-height 80px
-					font-size 16px
-					color #333
-					border none
-					border-bottom solid 1px #ddd
-					border-radius 0
-
-					&:disabled
-						opacity 0.5
-
-				> [ref='upload']
-				> [ref='drive']
-				.kao
-				.poll
-					display inline-block
-					padding 0
-					margin 0
-					width 48px
-					height 48px
-					font-size 20px
-					color #657786
-					background transparent
-					outline none
-					border none
-					border-radius 0
-					box-shadow none
-
-	</style>
-	<script lang="typescript">
-		import Sortable from 'sortablejs';
-		import getKao from '../../common/scripts/get-kao';
-
-		this.mixin('api');
-
-		this.wait = false;
-		this.uploadings = [];
-		this.files = [];
-		this.poll = false;
-
-		this.on('mount', () => {
-			this.$refs.uploader.on('uploaded', file => {
-				this.addFile(file);
-			});
-
-			this.$refs.uploader.on('change-uploads', uploads => {
-				this.$emit('change-uploading-files', uploads);
-			});
-
-			this.$refs.text.focus();
-
-			new Sortable(this.$refs.attaches, {
-				animation: 150
-			});
-		});
-
-		this.onkeydown = e => {
-			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
-		};
-
-		this.onpaste = e => {
-			Array.from(e.clipboardData.items).forEach(item => {
-				if (item.kind == 'file') {
-					this.upload(item.getAsFile());
-				}
-			});
-		};
-
-		this.selectFile = () => {
-			this.$refs.file.click();
-		};
-
-		this.selectFileFromDrive = () => {
-			const i = riot.mount(document.body.appendChild(document.createElement('mk-drive-selector')), {
-				multiple: true
-			})[0];
-			i.one('selected', files => {
-				files.forEach(this.addFile);
-			});
-		};
-
-		this.changeFile = () => {
-			Array.from(this.$refs.file.files).forEach(this.upload);
-		};
-
-		this.upload = file => {
-			this.$refs.uploader.upload(file);
-		};
-
-		this.addFile = file => {
-			file._remove = () => {
-				this.files = this.files.filter(x => x.id != file.id);
-				this.$emit('change-files', this.files);
-				this.update();
-			};
-
-			this.files.push(file);
-			this.$emit('change-files', this.files);
-			this.update();
-		};
-
-		this.removeFile = e => {
-			const file = e.item;
-			this.files = this.files.filter(x => x.id != file.id);
-			this.$emit('change-files', this.files);
-			this.update();
-		};
-
-		this.addPoll = () => {
-			this.poll = true;
-		};
-
-		this.onPollDestroyed = () => {
-			this.update({
-				poll: false
-			});
-		};
-
-		this.post = () => {
-			this.update({
-				wait: true
-			});
-
-			const files = [];
-
-			if (this.files.length > 0) {
-				Array.from(this.$refs.attaches.children).forEach(el => {
-					const id = el.getAttribute('data-id');
-					const file = this.files.find(f => f.id == id);
-					files.push(file);
-				});
-			}
-
-			this.api('posts/create', {
-				text: this.$refs.text.value == '' ? undefined : this.$refs.text.value,
-				media_ids: this.files.length > 0 ? files.map(f => f.id) : undefined,
-				reply_id: opts.reply ? opts.reply.id : undefined,
-				poll: this.poll ? this.$refs.poll.get() : undefined
-			}).then(data => {
-				this.$emit('post');
-				this.$destroy();
-			}).catch(err => {
-				this.update({
-					wait: false
-				});
-			});
-		};
-
-		this.cancel = () => {
-			this.$emit('cancel');
-			this.$destroy();
-		};
-
-		this.kao = () => {
-			this.$refs.text.value += getKao();
-		};
-	</script>
-</mk-post-form>
diff --git a/src/web/app/mobile/views/post-form.vue b/src/web/app/mobile/views/post-form.vue
new file mode 100644
index 000000000..49f6a94d8
--- /dev/null
+++ b/src/web/app/mobile/views/post-form.vue
@@ -0,0 +1,204 @@
+<template>
+<div class="mk-post-form">
+	<header>
+		<button class="cancel" @click="cancel">%fa:times%</button>
+		<div>
+			<span v-if="refs.text" class="text-count { over: refs.text.value.length > 1000 }">{ 1000 - refs.text.value.length }</span>
+			<button class="submit" @click="post">%i18n:mobile.tags.mk-post-form.submit%</button>
+		</div>
+	</header>
+	<div class="form">
+		<mk-post-preview v-if="opts.reply" post={ opts.reply }/>
+		<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ opts.reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.post-placeholder%' }></textarea>
+		<div class="attaches" show={ files.length != 0 }>
+			<ul class="files" ref="attaches">
+				<li class="file" each={ files } data-id={ id }>
+					<div class="img" style="background-image: url({ url + '?thumbnail&size=128' })" @click="removeFile"></div>
+				</li>
+			</ul>
+		</div>
+		<mk-poll-editor v-if="poll" ref="poll" ondestroy={ onPollDestroyed }/>
+		<mk-uploader @uploaded="attachMedia" @change="onChangeUploadings"/>
+		<button ref="upload" @click="selectFile">%fa:upload%</button>
+		<button ref="drive" @click="selectFileFromDrive">%fa:cloud%</button>
+		<button class="kao" @click="kao">%fa:R smile%</button>
+		<button class="poll" @click="addPoll">%fa:chart-pie%</button>
+		<input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
+	</div>
+</div
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Sortable from 'sortablejs';
+import getKao from '../../common/scripts/get-kao';
+
+export default Vue.extend({
+	data() {
+		return {
+			posting: false,
+			text: '',
+			uploadings: [],
+			files: [],
+			poll: false
+		};
+	},
+	mounted() {
+		(this.$refs.text as any).focus();
+
+		new Sortable(this.$refs.attaches, {
+			animation: 150
+		});
+	},
+	methods: {
+		attachMedia(driveFile) {
+			this.files.push(driveFile);
+			this.$emit('change-attached-media', this.files);
+		},
+		detachMedia(id) {
+			this.files = this.files.filter(x => x.id != id);
+			this.$emit('change-attached-media', this.files);
+		},
+		onChangeFile() {
+			Array.from((this.$refs.file as any).files).forEach(this.upload);
+		},
+		upload(file) {
+			(this.$refs.uploader as any).upload(file);
+		},
+		onChangeUploadings(uploads) {
+			this.$emit('change-uploadings', uploads);
+		},
+		clear() {
+			this.text = '';
+			this.files = [];
+			this.poll = false;
+			this.$emit('change-attached-media');
+		},
+		cancel() {
+			this.$emit('cancel');
+			this.$destroy();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-post-form
+	max-width 500px
+	width calc(100% - 16px)
+	margin 8px auto
+	background #fff
+	border-radius 8px
+	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+
+	@media (min-width 500px)
+		margin 16px auto
+		width calc(100% - 32px)
+
+	> header
+		z-index 1
+		height 50px
+		box-shadow 0 1px 0 0 rgba(0, 0, 0, 0.1)
+
+		> .cancel
+			width 50px
+			line-height 50px
+			font-size 24px
+			color #555
+
+		> div
+			position absolute
+			top 0
+			right 0
+
+			> .text-count
+				line-height 50px
+				color #657786
+
+			> .submit
+				margin 8px
+				padding 0 16px
+				line-height 34px
+				color $theme-color-foreground
+				background $theme-color
+				border-radius 4px
+
+				&:disabled
+					opacity 0.7
+
+	> .form
+		max-width 500px
+		margin 0 auto
+
+		> mk-post-preview
+			padding 16px
+
+		> .attaches
+
+			> .files
+				display block
+				margin 0
+				padding 4px
+				list-style none
+
+				&:after
+					content ""
+					display block
+					clear both
+
+				> .file
+					display block
+					float left
+					margin 0
+					padding 0
+					border solid 4px transparent
+
+					> .img
+						width 64px
+						height 64px
+						background-size cover
+						background-position center center
+
+		> mk-uploader
+			margin 8px 0 0 0
+			padding 8px
+
+		> [ref='file']
+			display none
+
+		> [ref='text']
+			display block
+			padding 12px
+			margin 0
+			width 100%
+			max-width 100%
+			min-width 100%
+			min-height 80px
+			font-size 16px
+			color #333
+			border none
+			border-bottom solid 1px #ddd
+			border-radius 0
+
+			&:disabled
+				opacity 0.5
+
+		> [ref='upload']
+		> [ref='drive']
+		.kao
+		.poll
+			display inline-block
+			padding 0
+			margin 0
+			width 48px
+			height 48px
+			font-size 20px
+			color #657786
+			background transparent
+			outline none
+			border none
+			border-radius 0
+			box-shadow none
+
+</style>
+

From 08d0c46d2838a3d6e7a5618f73dbc3961175d0a1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 13:35:51 +0900
Subject: [PATCH 095/286] wip

---
 src/web/app/mobile/tags/init-following.tag | 130 ---------------------
 src/web/app/mobile/views/friends-maker.vue | 126 ++++++++++++++++++++
 src/web/app/mobile/views/timeline.vue      |   2 +-
 3 files changed, 127 insertions(+), 131 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/init-following.tag
 create mode 100644 src/web/app/mobile/views/friends-maker.vue

diff --git a/src/web/app/mobile/tags/init-following.tag b/src/web/app/mobile/tags/init-following.tag
deleted file mode 100644
index bf8313872..000000000
--- a/src/web/app/mobile/tags/init-following.tag
+++ /dev/null
@@ -1,130 +0,0 @@
-<mk-init-following>
-	<p class="title">気になるユーザーをフォロー:</p>
-	<div class="users" v-if="!fetching && users.length > 0">
-		<template each={ users }>
-			<mk-user-card user={ this } />
-		</template>
-	</div>
-	<p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p>
-	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
-	<a class="refresh" @click="refresh">もっと見る</a>
-	<button class="close" @click="close" title="閉じる">%fa:times%</button>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border-radius 8px
-			box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
-
-			> .title
-				margin 0
-				padding 8px 16px
-				font-size 1em
-				font-weight bold
-				color #888
-
-			> .users
-				overflow-x scroll
-				-webkit-overflow-scrolling touch
-				white-space nowrap
-				padding 16px
-				background #eee
-
-				> mk-user-card
-					&:not(:last-child)
-						margin-right 16px
-
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-			> .fetching
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-			> .refresh
-				display block
-				margin 0
-				padding 8px 16px
-				text-align right
-				font-size 0.9em
-				color #999
-
-			> .close
-				cursor pointer
-				display block
-				position absolute
-				top 0
-				right 0
-				z-index 1
-				margin 0
-				padding 0
-				font-size 1.2em
-				color #999
-				border none
-				outline none
-				background transparent
-
-				&:hover
-					color #555
-
-				&:active
-					color #222
-
-				> [data-fa]
-					padding 10px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.users = null;
-		this.fetching = true;
-
-		this.limit = 6;
-		this.page = 0;
-
-		this.on('mount', () => {
-			this.fetch();
-		});
-
-		this.fetch = () => {
-			this.update({
-				fetching: true,
-				users: null
-			});
-
-			this.api('users/recommendation', {
-				limit: this.limit,
-				offset: this.limit * this.page
-			}).then(users => {
-				this.fetching = false
-				this.users = users
-				this.update({
-					fetching: false,
-					users: users
-				});
-			});
-		};
-
-		this.refresh = () => {
-			if (this.users.length < this.limit) {
-				this.page = 0;
-			} else {
-				this.page++;
-			}
-			this.fetch();
-		};
-
-		this.close = () => {
-			this.$destroy();
-		};
-	</script>
-</mk-init-following>
diff --git a/src/web/app/mobile/views/friends-maker.vue b/src/web/app/mobile/views/friends-maker.vue
new file mode 100644
index 000000000..a7a81aeb7
--- /dev/null
+++ b/src/web/app/mobile/views/friends-maker.vue
@@ -0,0 +1,126 @@
+<template>
+<div class="mk-friends-maker">
+	<p class="title">気になるユーザーをフォロー:</p>
+	<div class="users" v-if="!fetching && users.length > 0">
+		<template each={ users }>
+			<mk-user-card user={ this } />
+		</template>
+	</div>
+	<p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
+	<a class="refresh" @click="refresh">もっと見る</a>
+	<button class="close" @click="close" title="閉じる">%fa:times%</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			users: [],
+			fetching: true,
+			limit: 6,
+			page: 0
+		};
+	},
+	mounted() {
+		this.fetch();
+	},
+	methods: {
+		fetch() {
+			this.fetching = true;
+			this.users = [];
+
+			this.$root.$data.os.api('users/recommendation', {
+				limit: this.limit,
+				offset: this.limit * this.page
+			}).then(users => {
+				this.fetching = false;
+				this.users = users;
+			});
+		},
+		refresh() {
+			if (this.users.length < this.limit) {
+				this.page = 0;
+			} else {
+				this.page++;
+			}
+			this.fetch();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-friends-maker
+	background #fff
+	border-radius 8px
+	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+
+	> .title
+		margin 0
+		padding 8px 16px
+		font-size 1em
+		font-weight bold
+		color #888
+
+	> .users
+		overflow-x scroll
+		-webkit-overflow-scrolling touch
+		white-space nowrap
+		padding 16px
+		background #eee
+
+		> mk-user-card
+			&:not(:last-child)
+				margin-right 16px
+
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+	> .fetching
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+	> .refresh
+		display block
+		margin 0
+		padding 8px 16px
+		text-align right
+		font-size 0.9em
+		color #999
+
+	> .close
+		cursor pointer
+		display block
+		position absolute
+		top 0
+		right 0
+		z-index 1
+		margin 0
+		padding 0
+		font-size 1.2em
+		color #999
+		border none
+		outline none
+		background transparent
+
+		&:hover
+			color #555
+
+		&:active
+			color #222
+
+		> [data-fa]
+			padding 10px
+
+</style>
diff --git a/src/web/app/mobile/views/timeline.vue b/src/web/app/mobile/views/timeline.vue
index 3a5df7792..77c24a469 100644
--- a/src/web/app/mobile/views/timeline.vue
+++ b/src/web/app/mobile/views/timeline.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-timeline">
+	<mk-friends-maker v-if="alone"/>
 	<mk-posts ref="timeline" :posts="posts">
-		<mk-friends-maker v-if="alone" slot="head"/>
 		<div class="init" v-if="fetching">
 			%fa:spinner .pulse%%i18n:common.loading%
 		</div>

From 5a0b56f70228a43668b04654c8da41d70299482d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 14:04:18 +0900
Subject: [PATCH 096/286] wip

---
 src/web/app/common/-tags/ellipsis.tag         | 24 -----------------
 .../app/common/views/components/ellipsis.vue  | 26 +++++++++++++++++++
 2 files changed, 26 insertions(+), 24 deletions(-)
 delete mode 100644 src/web/app/common/-tags/ellipsis.tag
 create mode 100644 src/web/app/common/views/components/ellipsis.vue

diff --git a/src/web/app/common/-tags/ellipsis.tag b/src/web/app/common/-tags/ellipsis.tag
deleted file mode 100644
index 734454e4a..000000000
--- a/src/web/app/common/-tags/ellipsis.tag
+++ /dev/null
@@ -1,24 +0,0 @@
-<mk-ellipsis><span>.</span><span>.</span><span>.</span>
-	<style lang="stylus" scoped>
-		:scope
-			display inline
-
-			> span
-				animation ellipsis 1.4s infinite ease-in-out both
-
-				&:nth-child(1)
-					animation-delay 0s
-
-				&:nth-child(2)
-					animation-delay 0.16s
-
-				&:nth-child(3)
-					animation-delay 0.32s
-
-			@keyframes ellipsis
-				0%, 80%, 100%
-					opacity 1
-				40%
-					opacity 0
-	</style>
-</mk-ellipsis>
diff --git a/src/web/app/common/views/components/ellipsis.vue b/src/web/app/common/views/components/ellipsis.vue
new file mode 100644
index 000000000..07349902d
--- /dev/null
+++ b/src/web/app/common/views/components/ellipsis.vue
@@ -0,0 +1,26 @@
+<template>
+	<span class="mk-ellipsis">
+		<span>.</span><span>.</span><span>.</span>
+	</span>
+</template>
+
+<style lang="stylus" scoped>
+.mk-ellipsis
+	> span
+		animation ellipsis 1.4s infinite ease-in-out both
+
+		&:nth-child(1)
+			animation-delay 0s
+
+		&:nth-child(2)
+			animation-delay 0.16s
+
+		&:nth-child(3)
+			animation-delay 0.32s
+
+	@keyframes ellipsis
+		0%, 80%, 100%
+			opacity 1
+		40%
+			opacity 0
+</style>

From c273fb8c4210704cfb6a87111238e2145252d8b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 14:53:52 +0900
Subject: [PATCH 097/286] wip

---
 src/web/app/mobile/tags/ui.tag         | 419 -------------------------
 src/web/app/mobile/views/ui-header.vue | 169 ++++++++++
 src/web/app/mobile/views/ui-nav.vue    | 196 ++++++++++++
 src/web/app/mobile/views/ui.vue        |  57 ++++
 4 files changed, 422 insertions(+), 419 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/ui.tag
 create mode 100644 src/web/app/mobile/views/ui-header.vue
 create mode 100644 src/web/app/mobile/views/ui-nav.vue
 create mode 100644 src/web/app/mobile/views/ui.vue

diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
deleted file mode 100644
index 0a4483fd2..000000000
--- a/src/web/app/mobile/tags/ui.tag
+++ /dev/null
@@ -1,419 +0,0 @@
-<mk-ui>
-	<mk-ui-header/>
-	<mk-ui-nav ref="nav"/>
-	<div class="content">
-		<yield />
-	</div>
-	<mk-stream-indicator v-if="SIGNIN"/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding-top 48px
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.isDrawerOpening = false;
-
-		this.on('mount', () => {
-			this.connection.on('notification', this.onStreamNotification);
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('notification', this.onStreamNotification);
-			this.stream.dispose(this.connectionId);
-		});
-
-		this.toggleDrawer = () => {
-			this.isDrawerOpening = !this.isDrawerOpening;
-			this.$refs.nav.root.style.display = this.isDrawerOpening ? 'block' : 'none';
-		};
-
-		this.onStreamNotification = notification => {
-			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
-			this.connection.send({
-				type: 'read_notification',
-				id: notification.id
-			});
-
-			riot.mount(document.body.appendChild(document.createElement('mk-notify')), {
-				notification: notification
-			});
-		};
-	</script>
-</mk-ui>
-
-<mk-ui-header>
-	<mk-special-message/>
-	<div class="main">
-		<div class="backdrop"></div>
-		<div class="content">
-			<button class="nav" @click="parent.toggleDrawer">%fa:bars%</button>
-			<template v-if="hasUnreadNotifications || hasUnreadMessagingMessages">%fa:circle%</template>
-			<h1 ref="title">Misskey</h1>
-			<button v-if="func" @click="func"><mk-raw content={ funcIcon }/></button>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			$height = 48px
-
-			display block
-			position fixed
-			top 0
-			z-index 1024
-			width 100%
-			box-shadow 0 1px 0 rgba(#000, 0.075)
-
-			> .main
-				color rgba(#fff, 0.9)
-
-				> .backdrop
-					position absolute
-					top 0
-					z-index 1023
-					width 100%
-					height $height
-					-webkit-backdrop-filter blur(12px)
-					backdrop-filter blur(12px)
-					background-color rgba(#1b2023, 0.75)
-
-				> .content
-					z-index 1024
-
-					> h1
-						display block
-						margin 0 auto
-						padding 0
-						width 100%
-						max-width calc(100% - 112px)
-						text-align center
-						font-size 1.1em
-						font-weight normal
-						line-height $height
-						white-space nowrap
-						overflow hidden
-						text-overflow ellipsis
-
-						[data-fa]
-							margin-right 8px
-
-						> img
-							display inline-block
-							vertical-align bottom
-							width ($height - 16px)
-							height ($height - 16px)
-							margin 8px
-							border-radius 6px
-
-					> .nav
-						display block
-						position absolute
-						top 0
-						left 0
-						width $height
-						font-size 1.4em
-						line-height $height
-						border-right solid 1px rgba(#000, 0.1)
-
-						> [data-fa]
-							transition all 0.2s ease
-
-					> [data-fa].circle
-						position absolute
-						top 8px
-						left 8px
-						pointer-events none
-						font-size 10px
-						color $theme-color
-
-					> button:last-child
-						display block
-						position absolute
-						top 0
-						right 0
-						width $height
-						text-align center
-						font-size 1.4em
-						color inherit
-						line-height $height
-						border-left solid 1px rgba(#000, 0.1)
-
-	</style>
-	<script lang="typescript">
-		import ui from '../scripts/ui-event';
-
-		this.mixin('api');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.func = null;
-		this.funcIcon = null;
-
-		this.on('mount', () => {
-			this.connection.on('read_all_notifications', this.onReadAllNotifications);
-			this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
-
-			// Fetch count of unread notifications
-			this.api('notifications/get_unread_count').then(res => {
-				if (res.count > 0) {
-					this.update({
-						hasUnreadNotifications: true
-					});
-				}
-			});
-
-			// Fetch count of unread messaging messages
-			this.api('messaging/unread').then(res => {
-				if (res.count > 0) {
-					this.update({
-						hasUnreadMessagingMessages: true
-					});
-				}
-			});
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('read_all_notifications', this.onReadAllNotifications);
-			this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
-			this.stream.dispose(this.connectionId);
-
-			ui.off('title', this.setTitle);
-			ui.off('func', this.setFunc);
-		});
-
-		this.onReadAllNotifications = () => {
-			this.update({
-				hasUnreadNotifications: false
-			});
-		};
-
-		this.onReadAllMessagingMessages = () => {
-			this.update({
-				hasUnreadMessagingMessages: false
-			});
-		};
-
-		this.onUnreadMessagingMessage = () => {
-			this.update({
-				hasUnreadMessagingMessages: true
-			});
-		};
-
-		this.setTitle = title => {
-			this.$refs.title.innerHTML = title;
-		};
-
-		this.setFunc = (fn, icon) => {
-			this.update({
-				func: fn,
-				funcIcon: icon
-			});
-		};
-
-		ui.on('title', this.setTitle);
-		ui.on('func', this.setFunc);
-	</script>
-</mk-ui-header>
-
-<mk-ui-nav>
-	<div class="backdrop" @click="parent.toggleDrawer"></div>
-	<div class="body">
-		<a class="me" v-if="SIGNIN" href={ '/' + I.username }>
-			<img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/>
-			<p class="name">{ I.name }</p>
-		</a>
-		<div class="links">
-			<ul>
-				<li><a href="/">%fa:home%%i18n:mobile.tags.mk-ui-nav.home%%fa:angle-right%</a></li>
-				<li><a href="/i/notifications">%fa:R bell%%i18n:mobile.tags.mk-ui-nav.notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</a></li>
-				<li><a href="/i/messaging">%fa:R comments%%i18n:mobile.tags.mk-ui-nav.messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</a></li>
-			</ul>
-			<ul>
-				<li><a href={ _CH_URL_ } target="_blank">%fa:tv%%i18n:mobile.tags.mk-ui-nav.ch%%fa:angle-right%</a></li>
-				<li><a href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-ui-nav.drive%%fa:angle-right%</a></li>
-			</ul>
-			<ul>
-				<li><a @click="search">%fa:search%%i18n:mobile.tags.mk-ui-nav.search%%fa:angle-right%</a></li>
-			</ul>
-			<ul>
-				<li><a href="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</a></li>
-			</ul>
-		</div>
-		<a href={ aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display none
-
-			.backdrop
-				position fixed
-				top 0
-				left 0
-				z-index 1025
-				width 100%
-				height 100%
-				background rgba(0, 0, 0, 0.2)
-
-			.body
-				position fixed
-				top 0
-				left 0
-				z-index 1026
-				width 240px
-				height 100%
-				overflow auto
-				-webkit-overflow-scrolling touch
-				color #777
-				background #fff
-
-			.me
-				display block
-				margin 0
-				padding 16px
-
-				.avatar
-					display inline
-					max-width 64px
-					border-radius 32px
-					vertical-align middle
-
-				.name
-					display block
-					margin 0 16px
-					position absolute
-					top 0
-					left 80px
-					padding 0
-					width calc(100% - 112px)
-					color #777
-					line-height 96px
-					overflow hidden
-					text-overflow ellipsis
-					white-space nowrap
-
-			ul
-				display block
-				margin 16px 0
-				padding 0
-				list-style none
-
-				&:first-child
-					margin-top 0
-
-				li
-					display block
-					font-size 1em
-					line-height 1em
-
-					a
-						display block
-						padding 0 20px
-						line-height 3rem
-						line-height calc(1rem + 30px)
-						color #777
-						text-decoration none
-
-						> [data-fa]:first-child
-							margin-right 0.5em
-
-						> [data-fa].circle
-							margin-left 6px
-							font-size 10px
-							color $theme-color
-
-						> [data-fa]:last-child
-							position absolute
-							top 0
-							right 0
-							padding 0 20px
-							font-size 1.2em
-							line-height calc(1rem + 30px)
-							color #ccc
-
-			.about
-				margin 0
-				padding 1em 0
-				text-align center
-				font-size 0.8em
-				opacity 0.5
-
-				a
-					color #777
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-		this.mixin('page');
-		this.mixin('api');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/about`;
-
-		this.on('mount', () => {
-			this.connection.on('read_all_notifications', this.onReadAllNotifications);
-			this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
-
-			// Fetch count of unread notifications
-			this.api('notifications/get_unread_count').then(res => {
-				if (res.count > 0) {
-					this.update({
-						hasUnreadNotifications: true
-					});
-				}
-			});
-
-			// Fetch count of unread messaging messages
-			this.api('messaging/unread').then(res => {
-				if (res.count > 0) {
-					this.update({
-						hasUnreadMessagingMessages: true
-					});
-				}
-			});
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('read_all_notifications', this.onReadAllNotifications);
-			this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
-			this.stream.dispose(this.connectionId);
-		});
-
-		this.onReadAllNotifications = () => {
-			this.update({
-				hasUnreadNotifications: false
-			});
-		};
-
-		this.onReadAllMessagingMessages = () => {
-			this.update({
-				hasUnreadMessagingMessages: false
-			});
-		};
-
-		this.onUnreadMessagingMessage = () => {
-			this.update({
-				hasUnreadMessagingMessages: true
-			});
-		};
-
-		this.search = () => {
-			const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%');
-			if (query == null || query == '') return;
-			this.page('/search?q=' + encodeURIComponent(query));
-		};
-	</script>
-</mk-ui-nav>
diff --git a/src/web/app/mobile/views/ui-header.vue b/src/web/app/mobile/views/ui-header.vue
new file mode 100644
index 000000000..176751a66
--- /dev/null
+++ b/src/web/app/mobile/views/ui-header.vue
@@ -0,0 +1,169 @@
+<template>
+<div class="mk-ui-header">
+	<mk-special-message/>
+	<div class="main">
+		<div class="backdrop"></div>
+		<div class="content">
+			<button class="nav" @click="parent.toggleDrawer">%fa:bars%</button>
+			<template v-if="hasUnreadNotifications || hasUnreadMessagingMessages">%fa:circle%</template>
+			<h1 ref="title">Misskey</h1>
+			<button v-if="func" @click="func"><mk-raw content={ funcIcon }/></button>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	data() {
+		return {
+			func: null,
+			funcIcon: null,
+			hasUnreadNotifications: false,
+			hasUnreadMessagingMessages: false,
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		if (this.$root.$data.os.isSignedIn) {
+			this.connection = this.$root.$data.os.stream.getConnection();
+			this.connectionId = this.$root.$data.os.stream.use();
+
+			this.connection.on('read_all_notifications', this.onReadAllNotifications);
+			this.connection.on('unread_notification', this.onUnreadNotification);
+			this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
+
+			// Fetch count of unread notifications
+			this.$root.$data.os.api('notifications/get_unread_count').then(res => {
+				if (res.count > 0) {
+					this.hasUnreadNotifications = true;
+				}
+			});
+
+			// Fetch count of unread messaging messages
+			this.$root.$data.os.api('messaging/unread').then(res => {
+				if (res.count > 0) {
+					this.hasUnreadMessagingMessages = true;
+				}
+			});
+		}
+	},
+	beforeDestroy() {
+		if (this.$root.$data.os.isSignedIn) {
+			this.connection.off('read_all_notifications', this.onReadAllNotifications);
+			this.connection.off('unread_notification', this.onUnreadNotification);
+			this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
+			this.$root.$data.os.stream.dispose(this.connectionId);
+		}
+	},
+	methods: {
+		setFunc(fn, icon) {
+			this.func = fn;
+			this.funcIcon = icon;
+		},
+		onReadAllNotifications() {
+			this.hasUnreadNotifications = false;
+		},
+		onUnreadNotification() {
+			this.hasUnreadNotifications = true;
+		},
+		onReadAllMessagingMessages() {
+			this.hasUnreadMessagingMessages = false;
+		},
+		onUnreadMessagingMessage() {
+			this.hasUnreadMessagingMessages = true;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-ui-header
+	$height = 48px
+
+	position fixed
+	top 0
+	z-index 1024
+	width 100%
+	box-shadow 0 1px 0 rgba(#000, 0.075)
+
+	> .main
+		color rgba(#fff, 0.9)
+
+		> .backdrop
+			position absolute
+			top 0
+			z-index 1023
+			width 100%
+			height $height
+			-webkit-backdrop-filter blur(12px)
+			backdrop-filter blur(12px)
+			background-color rgba(#1b2023, 0.75)
+
+		> .content
+			z-index 1024
+
+			> h1
+				display block
+				margin 0 auto
+				padding 0
+				width 100%
+				max-width calc(100% - 112px)
+				text-align center
+				font-size 1.1em
+				font-weight normal
+				line-height $height
+				white-space nowrap
+				overflow hidden
+				text-overflow ellipsis
+
+				[data-fa]
+					margin-right 8px
+
+				> img
+					display inline-block
+					vertical-align bottom
+					width ($height - 16px)
+					height ($height - 16px)
+					margin 8px
+					border-radius 6px
+
+			> .nav
+				display block
+				position absolute
+				top 0
+				left 0
+				width $height
+				font-size 1.4em
+				line-height $height
+				border-right solid 1px rgba(#000, 0.1)
+
+				> [data-fa]
+					transition all 0.2s ease
+
+			> [data-fa].circle
+				position absolute
+				top 8px
+				left 8px
+				pointer-events none
+				font-size 10px
+				color $theme-color
+
+			> button:last-child
+				display block
+				position absolute
+				top 0
+				right 0
+				width $height
+				text-align center
+				font-size 1.4em
+				color inherit
+				line-height $height
+				border-left solid 1px rgba(#000, 0.1)
+
+</style>
diff --git a/src/web/app/mobile/views/ui-nav.vue b/src/web/app/mobile/views/ui-nav.vue
new file mode 100644
index 000000000..3765ce887
--- /dev/null
+++ b/src/web/app/mobile/views/ui-nav.vue
@@ -0,0 +1,196 @@
+<template>
+<div class="mk-ui-nav" :style="{ display: isOpen ? 'block' : 'none' }">
+	<div class="backdrop" @click="parent.toggleDrawer"></div>
+	<div class="body">
+		<a class="me" v-if="SIGNIN" href={ '/' + I.username }>
+			<img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/>
+			<p class="name">{ I.name }</p>
+		</a>
+		<div class="links">
+			<ul>
+				<li><a href="/">%fa:home%%i18n:mobile.tags.mk-ui-nav.home%%fa:angle-right%</a></li>
+				<li><a href="/i/notifications">%fa:R bell%%i18n:mobile.tags.mk-ui-nav.notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</a></li>
+				<li><a href="/i/messaging">%fa:R comments%%i18n:mobile.tags.mk-ui-nav.messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</a></li>
+			</ul>
+			<ul>
+				<li><a href={ _CH_URL_ } target="_blank">%fa:tv%%i18n:mobile.tags.mk-ui-nav.ch%%fa:angle-right%</a></li>
+				<li><a href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-ui-nav.drive%%fa:angle-right%</a></li>
+			</ul>
+			<ul>
+				<li><a @click="search">%fa:search%%i18n:mobile.tags.mk-ui-nav.search%%fa:angle-right%</a></li>
+			</ul>
+			<ul>
+				<li><a href="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</a></li>
+			</ul>
+		</div>
+		<a href={ aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	data() {
+		return {
+			hasUnreadNotifications: false,
+			hasUnreadMessagingMessages: false,
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		if (this.$root.$data.os.isSignedIn) {
+			this.connection = this.$root.$data.os.stream.getConnection();
+			this.connectionId = this.$root.$data.os.stream.use();
+
+			this.connection.on('read_all_notifications', this.onReadAllNotifications);
+			this.connection.on('unread_notification', this.onUnreadNotification);
+			this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
+
+			// Fetch count of unread notifications
+			this.$root.$data.os.api('notifications/get_unread_count').then(res => {
+				if (res.count > 0) {
+					this.hasUnreadNotifications = true;
+				}
+			});
+
+			// Fetch count of unread messaging messages
+			this.$root.$data.os.api('messaging/unread').then(res => {
+				if (res.count > 0) {
+					this.hasUnreadMessagingMessages = true;
+				}
+			});
+		}
+	},
+	beforeDestroy() {
+		if (this.$root.$data.os.isSignedIn) {
+			this.connection.off('read_all_notifications', this.onReadAllNotifications);
+			this.connection.off('unread_notification', this.onUnreadNotification);
+			this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
+			this.$root.$data.os.stream.dispose(this.connectionId);
+		}
+	},
+	methods: {
+		search() {
+			const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%');
+			if (query == null || query == '') return;
+			this.page('/search?q=' + encodeURIComponent(query));
+		},
+		onReadAllNotifications() {
+			this.hasUnreadNotifications = false;
+		},
+		onUnreadNotification() {
+			this.hasUnreadNotifications = true;
+		},
+		onReadAllMessagingMessages() {
+			this.hasUnreadMessagingMessages = false;
+		},
+		onUnreadMessagingMessage() {
+			this.hasUnreadMessagingMessages = true;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-ui-nav
+	.backdrop
+		position fixed
+		top 0
+		left 0
+		z-index 1025
+		width 100%
+		height 100%
+		background rgba(0, 0, 0, 0.2)
+
+	.body
+		position fixed
+		top 0
+		left 0
+		z-index 1026
+		width 240px
+		height 100%
+		overflow auto
+		-webkit-overflow-scrolling touch
+		color #777
+		background #fff
+
+	.me
+		display block
+		margin 0
+		padding 16px
+
+		.avatar
+			display inline
+			max-width 64px
+			border-radius 32px
+			vertical-align middle
+
+		.name
+			display block
+			margin 0 16px
+			position absolute
+			top 0
+			left 80px
+			padding 0
+			width calc(100% - 112px)
+			color #777
+			line-height 96px
+			overflow hidden
+			text-overflow ellipsis
+			white-space nowrap
+
+	ul
+		display block
+		margin 16px 0
+		padding 0
+		list-style none
+
+		&:first-child
+			margin-top 0
+
+		li
+			display block
+			font-size 1em
+			line-height 1em
+
+			a
+				display block
+				padding 0 20px
+				line-height 3rem
+				line-height calc(1rem + 30px)
+				color #777
+				text-decoration none
+
+				> [data-fa]:first-child
+					margin-right 0.5em
+
+				> [data-fa].circle
+					margin-left 6px
+					font-size 10px
+					color $theme-color
+
+				> [data-fa]:last-child
+					position absolute
+					top 0
+					right 0
+					padding 0 20px
+					font-size 1.2em
+					line-height calc(1rem + 30px)
+					color #ccc
+
+	.about
+		margin 0
+		padding 1em 0
+		text-align center
+		font-size 0.8em
+		opacity 0.5
+
+		a
+			color #777
+
+</style>
diff --git a/src/web/app/mobile/views/ui.vue b/src/web/app/mobile/views/ui.vue
new file mode 100644
index 000000000..aa5e2457c
--- /dev/null
+++ b/src/web/app/mobile/views/ui.vue
@@ -0,0 +1,57 @@
+<template>
+<div class="mk-ui">
+	<mk-ui-header/>
+	<mk-ui-nav :is-open="isDrawerOpening"/>
+	<div class="content">
+		<slot></slot>
+	</div>
+	<mk-stream-indicator v-if="$root.$data.os.isSignedIn"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			isDrawerOpening: false,
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		if (this.$root.$data.os.isSignedIn) {
+			this.connection = this.$root.$data.os.stream.getConnection();
+			this.connectionId = this.$root.$data.os.stream.use();
+
+			this.connection.on('notification', this.onNotification);
+		}
+	},
+	beforeDestroy() {
+		if (this.$root.$data.os.isSignedIn) {
+			this.connection.off('notification', this.onNotification);
+			this.$root.$data.os.stream.dispose(this.connectionId);
+		}
+	},
+	methods: {
+		onNotification(notification) {
+			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
+			this.connection.send({
+				type: 'read_notification',
+				id: notification.id
+			});
+
+			document.body.appendChild(new MkNotify({
+				propsData: {
+					notification
+				}
+			}).$mount().$el);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-ui
+	padding-top 48px
+</style>

From 92e9f02e21f384fe7a81556bf3cf0468a404bb0a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 15:54:18 +0900
Subject: [PATCH 098/286] wip

---
 .../components/drive.vue}                     | 826 +++++++++---------
 1 file changed, 400 insertions(+), 426 deletions(-)
 rename src/web/app/desktop/{-tags/drive/browser.tag => views/components/drive.vue} (51%)

diff --git a/src/web/app/desktop/-tags/drive/browser.tag b/src/web/app/desktop/views/components/drive.vue
similarity index 51%
rename from src/web/app/desktop/-tags/drive/browser.tag
rename to src/web/app/desktop/views/components/drive.vue
index 7aaedab82..5d398dab9 100644
--- a/src/web/app/desktop/-tags/drive/browser.tag
+++ b/src/web/app/desktop/views/components/drive.vue
@@ -1,6 +1,7 @@
-<mk-drive-browser>
+<template>
+<div class="mk-drive">
 	<nav>
-		<div class="path" oncontextmenu={ pathOncontextmenu }>
+		<div class="path" @contextmenu.prevent.stop="() => {}">
 			<mk-drive-browser-nav-folder :class="{ current: folder == null }" folder={ null }/>
 			<template each={ folder in hierarchyFolders }>
 				<span class="separator">%fa:angle-right%</span>
@@ -11,7 +12,15 @@
 		</div>
 		<input class="search" type="search" placeholder="&#xf002; %i18n:desktop.tags.mk-drive-browser.search%"/>
 	</nav>
-	<div class="main { uploading: uploads.length > 0, fetching: fetching }" ref="main" onmousedown={ onmousedown } ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop } oncontextmenu={ oncontextmenu }>
+	<div class="main { uploading: uploads.length > 0, fetching: fetching }"
+		ref="main"
+		@mousedown="onMousedown"
+		@dragover.prevent.stop="onDragover"
+		@dragenter.prevent="onDragenter"
+		@dragleave="onDragleave"
+		@drop.prevent.stop="onDrop"
+		@contextmenu.prevent.stop="onContextmenu"
+	>
 		<div class="selection" ref="selection"></div>
 		<div class="contents" ref="contents">
 			<div class="folders" ref="foldersContainer" v-if="folders.length > 0">
@@ -44,323 +53,142 @@
 		</div>
 	</div>
 	<div class="dropzone" v-if="draghover"></div>
-	<mk-uploader ref="uploader"/>
-	<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" onchange={ changeFileInput }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
+	<mk-uploader @change="onChangeUploaderUploads" @uploaded="onUploaderUploaded"/>
+	<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/>
+</div>
+</template>
 
-			> nav
-				display block
-				z-index 2
-				width 100%
-				overflow auto
-				font-size 0.9em
-				color #555
-				background #fff
-				//border-bottom 1px solid #dfdfdf
-				box-shadow 0 1px 0 rgba(0, 0, 0, 0.05)
+<script lang="ts">
+import Vue from 'vue';
+import contains from '../../../common/scripts/contains';
+import dialog from '../../scripts/dialog';
+import inputDialog from '../../scripts/input-dialog';
 
-				&, *
-					user-select none
+export default Vue.extend({
+	props: {
+		initFolder: {
+			required: false
+		},
+		multiple: {
+			default: false
+		}
+	},
+	data() {
+		return {
+			/**
+			 * 現在の階層(フォルダ)
+			 * * null でルートを表す
+			 */
+			folder: null,
 
-				> .path
-					display inline-block
-					vertical-align bottom
-					margin 0
-					padding 0 8px
-					width calc(100% - 200px)
-					line-height 38px
-					white-space nowrap
+			files: [],
+			folders: [],
+			moreFiles: false,
+			moreFolders: false,
+			hierarchyFolders: [],
+			selectedFiles: [],
+			uploadings: [],
+			connection: null,
+			connectionId: null,
 
-					> *
-						display inline-block
-						margin 0
-						padding 0 8px
-						line-height 38px
-						cursor pointer
+			/**
+			 * ドロップされようとしているか
+			 */
+			draghover: false,
 
-						i
-							margin-right 4px
+			/**
+			 * 自信の所有するアイテムがドラッグをスタートさせたか
+			 * (自分自身の階層にドロップできないようにするためのフラグ)
+			 */
+			isDragSource: false,
 
-						*
-							pointer-events none
-
-						&:hover
-							text-decoration underline
-
-						&.current
-							font-weight bold
-							cursor default
-
-							&:hover
-								text-decoration none
-
-						&.separator
-							margin 0
-							padding 0
-							opacity 0.5
-							cursor default
-
-							> [data-fa]
-								margin 0
-
-				> .search
-					display inline-block
-					vertical-align bottom
-					user-select text
-					cursor auto
-					margin 0
-					padding 0 18px
-					width 200px
-					font-size 1em
-					line-height 38px
-					background transparent
-					outline none
-					//border solid 1px #ddd
-					border none
-					border-radius 0
-					box-shadow none
-					transition color 0.5s ease, border 0.5s ease
-					font-family FontAwesome, sans-serif
-
-					&[data-active='true']
-						background #fff
-
-					&::-webkit-input-placeholder,
-					&:-ms-input-placeholder,
-					&:-moz-placeholder
-						color $ui-control-foreground-color
-
-			> .main
-				padding 8px
-				height calc(100% - 38px)
-				overflow auto
-
-				&, *
-					user-select none
-
-				&.fetching
-					cursor wait !important
-
-					*
-						pointer-events none
-
-					> .contents
-						opacity 0.5
-
-				&.uploading
-					height calc(100% - 38px - 100px)
-
-				> .selection
-					display none
-					position absolute
-					z-index 128
-					top 0
-					left 0
-					border solid 1px $theme-color
-					background rgba($theme-color, 0.5)
-					pointer-events none
-
-				> .contents
-
-					> .folders
-					> .files
-						display flex
-						flex-wrap wrap
-
-						> .folder
-						> .file
-							flex-grow 1
-							width 144px
-							margin 4px
-
-						> .padding
-							flex-grow 1
-							pointer-events none
-							width 144px + 8px // 8px is margin
-
-					> .empty
-						padding 16px
-						text-align center
-						color #999
-						pointer-events none
-
-						> p
-							margin 0
-
-				> .fetching
-					.spinner
-						margin 100px auto
-						width 40px
-						height 40px
-						text-align center
-
-						animation sk-rotate 2.0s infinite linear
-
-					.dot1, .dot2
-						width 60%
-						height 60%
-						display inline-block
-						position absolute
-						top 0
-						background-color rgba(0, 0, 0, 0.3)
-						border-radius 100%
-
-						animation sk-bounce 2.0s infinite ease-in-out
-
-					.dot2
-						top auto
-						bottom 0
-						animation-delay -1.0s
-
-					@keyframes sk-rotate { 100% { transform: rotate(360deg); }}
-
-					@keyframes sk-bounce {
-						0%, 100% {
-							transform: scale(0.0);
-						} 50% {
-							transform: scale(1.0);
-						}
-					}
-
-			> .dropzone
-				position absolute
-				left 0
-				top 38px
-				width 100%
-				height calc(100% - 38px)
-				border dashed 2px rgba($theme-color, 0.5)
-				pointer-events none
-
-			> mk-uploader
-				height 100px
-				padding 16px
-				background #fff
-
-			> input
-				display none
-
-	</style>
-	<script lang="typescript">
-		import contains from '../../../common/scripts/contains';
-		import dialog from '../../scripts/dialog';
-		import inputDialog from '../../scripts/input-dialog';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.mixin('drive-stream');
-		this.connection = this.driveStream.getConnection();
-		this.connectionId = this.driveStream.use();
-
-		this.files = [];
-		this.folders = [];
-		this.hierarchyFolders = [];
-		this.selectedFiles = [];
-
-		this.uploads = [];
-
-		// 現在の階層(フォルダ)
-		// * null でルートを表す
-		this.folder = null;
-
-		this.multiple = this.opts.multiple != null ? this.opts.multiple : false;
-
-		// ドロップされようとしているか
-		this.draghover = false;
-
-		// 自信の所有するアイテムがドラッグをスタートさせたか
-		// (自分自身の階層にドロップできないようにするためのフラグ)
-		this.isDragSource = false;
-
-		this.on('mount', () => {
-			this.$refs.uploader.on('uploaded', file => {
-				this.addFile(file, true);
-			});
-
-			this.$refs.uploader.on('change-uploads', uploads => {
-				this.update({
-					uploads: uploads
-				});
-			});
-
-			this.connection.on('file_created', this.onStreamDriveFileCreated);
-			this.connection.on('file_updated', this.onStreamDriveFileUpdated);
-			this.connection.on('folder_created', this.onStreamDriveFolderCreated);
-			this.connection.on('folder_updated', this.onStreamDriveFolderUpdated);
-
-			if (this.opts.folder) {
-				this.move(this.opts.folder);
-			} else {
-				this.fetch();
-			}
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('file_created', this.onStreamDriveFileCreated);
-			this.connection.off('file_updated', this.onStreamDriveFileUpdated);
-			this.connection.off('folder_created', this.onStreamDriveFolderCreated);
-			this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
-			this.driveStream.dispose(this.connectionId);
-		});
-
-		this.onStreamDriveFileCreated = file => {
-			this.addFile(file, true);
+			fetching: true
 		};
+	},
+	mounted() {
+		this.connection = this.$root.$data.os.streams.driveStream.getConnection();
+		this.connectionId = this.$root.$data.os.streams.driveStream.use();
 
-		this.onStreamDriveFileUpdated = file => {
+		this.connection.on('file_created', this.onStreamDriveFileCreated);
+		this.connection.on('file_updated', this.onStreamDriveFileUpdated);
+		this.connection.on('folder_created', this.onStreamDriveFolderCreated);
+		this.connection.on('folder_updated', this.onStreamDriveFolderUpdated);
+
+		if (this.initFolder) {
+			this.move(this.initFolder);
+		} else {
+			this.fetch();
+		}
+	},
+	beforeDestroy() {
+		this.connection.off('file_created', this.onStreamDriveFileCreated);
+		this.connection.off('file_updated', this.onStreamDriveFileUpdated);
+		this.connection.off('folder_created', this.onStreamDriveFolderCreated);
+		this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
+		this.$root.$data.os.streams.driveStream.dispose(this.connectionId);
+	},
+	methods: {
+		onStreamDriveFileCreated(file) {
+			this.addFile(file, true);
+		},
+		onStreamDriveFileUpdated(file) {
 			const current = this.folder ? this.folder.id : null;
 			if (current != file.folder_id) {
 				this.removeFile(file);
 			} else {
 				this.addFile(file, true);
 			}
-		};
-
-		this.onStreamDriveFolderCreated = folder => {
+		},
+		onStreamDriveFolderCreated(folder) {
 			this.addFolder(folder, true);
-		};
-
-		this.onStreamDriveFolderUpdated = folder => {
+		},
+		onStreamDriveFolderUpdated(folder) {
 			const current = this.folder ? this.folder.id : null;
 			if (current != folder.parent_id) {
 				this.removeFolder(folder);
 			} else {
 				this.addFolder(folder, true);
 			}
-		};
-
-		this.onmousedown = e => {
+		},
+		onChangeUploaderUploads(uploads) {
+			this.uploadings = uploads;
+		},
+		onUploaderUploaded(file) {
+			this.addFile(file, true);
+		},
+		onMousedown(e): any {
 			if (contains(this.$refs.foldersContainer, e.target) || contains(this.$refs.filesContainer, e.target)) return true;
 
-			const rect = this.$refs.main.getBoundingClientRect();
+			const main = this.$refs.main as any;
+			const selection = this.$refs.selection as any;
 
-			const left = e.pageX + this.$refs.main.scrollLeft - rect.left - window.pageXOffset
-			const top = e.pageY + this.$refs.main.scrollTop - rect.top - window.pageYOffset
+			const rect = main.getBoundingClientRect();
+
+			const left = e.pageX + main.scrollLeft - rect.left - window.pageXOffset
+			const top = e.pageY + main.scrollTop - rect.top - window.pageYOffset
 
 			const move = e => {
-				this.$refs.selection.style.display = 'block';
+				selection.style.display = 'block';
 
-				const cursorX = e.pageX + this.$refs.main.scrollLeft - rect.left - window.pageXOffset;
-				const cursorY = e.pageY + this.$refs.main.scrollTop - rect.top - window.pageYOffset;
+				const cursorX = e.pageX + main.scrollLeft - rect.left - window.pageXOffset;
+				const cursorY = e.pageY + main.scrollTop - rect.top - window.pageYOffset;
 				const w = cursorX - left;
 				const h = cursorY - top;
 
 				if (w > 0) {
-					this.$refs.selection.style.width = w + 'px';
-					this.$refs.selection.style.left = left + 'px';
+					selection.style.width = w + 'px';
+					selection.style.left = left + 'px';
 				} else {
-					this.$refs.selection.style.width = -w + 'px';
-					this.$refs.selection.style.left = cursorX + 'px';
+					selection.style.width = -w + 'px';
+					selection.style.left = cursorX + 'px';
 				}
 
 				if (h > 0) {
-					this.$refs.selection.style.height = h + 'px';
-					this.$refs.selection.style.top = top + 'px';
+					selection.style.height = h + 'px';
+					selection.style.top = top + 'px';
 				} else {
-					this.$refs.selection.style.height = -h + 'px';
-					this.$refs.selection.style.top = cursorY + 'px';
+					selection.style.height = -h + 'px';
+					selection.style.top = cursorY + 'px';
 				}
 			};
 
@@ -368,23 +196,13 @@
 				document.documentElement.removeEventListener('mousemove', move);
 				document.documentElement.removeEventListener('mouseup', up);
 
-				this.$refs.selection.style.display = 'none';
+				selection.style.display = 'none';
 			};
 
 			document.documentElement.addEventListener('mousemove', move);
 			document.documentElement.addEventListener('mouseup', up);
-		};
-
-		this.pathOncontextmenu = e => {
-			e.preventDefault();
-			e.stopImmediatePropagation();
-			return false;
-		};
-
-		this.ondragover = e => {
-			e.preventDefault();
-			e.stopPropagation();
-
+		},
+		onDragover(e): any {
 			// ドラッグ元が自分自身の所有するアイテムかどうか
 			if (!this.isDragSource) {
 				// ドラッグされてきたものがファイルだったら
@@ -395,21 +213,14 @@
 				e.dataTransfer.dropEffect = 'none';
 				return false;
 			}
-		};
-
-		this.ondragenter = e => {
-			e.preventDefault();
+		},
+		onDragenter(e) {
 			if (!this.isDragSource) this.draghover = true;
-		};
-
-		this.ondragleave = e => {
+		},
+		onDragleave(e) {
 			this.draghover = false;
-		};
-
-		this.ondrop = e => {
-			e.preventDefault();
-			e.stopPropagation();
-
+		},
+		onDrop(e): any {
 			this.draghover = false;
 
 			// ドロップされてきたものがファイルだったら
@@ -433,7 +244,7 @@
 				const file = obj.id;
 				if (this.files.some(f => f.id == file)) return false;
 				this.removeFile(file);
-				this.api('drive/files/update', {
+				this.$root.$data.os.api('drive/files/update', {
 					file_id: file,
 					folder_id: this.folder ? this.folder.id : null
 				});
@@ -444,7 +255,7 @@
 				if (this.folder && folder == this.folder.id) return false;
 				if (this.folders.some(f => f.id == folder)) return false;
 				this.removeFolder(folder);
-				this.api('drive/folders/update', {
+				this.$root.$data.os.api('drive/folders/update', {
 					folder_id: folder,
 					parent_id: this.folder ? this.folder.id : null
 				}).then(() => {
@@ -464,32 +275,26 @@
 			}
 
 			return false;
-		};
-
-		this.oncontextmenu = e => {
-			e.preventDefault();
-			e.stopImmediatePropagation();
-
-			const ctx = riot.mount(document.body.appendChild(document.createElement('mk-drive-browser-base-contextmenu')), {
-				browser: this
-			})[0];
-			ctx.open({
-				x: e.pageX - window.pageXOffset,
-				y: e.pageY - window.pageYOffset
-			});
+		},
+		onContextmenu(e) {
+			document.body.appendChild(new MkDriveContextmenu({
+				propsData: {
+					browser: this,
+					x: e.pageX - window.pageXOffset,
+					y: e.pageY - window.pageYOffset
+				}
+			}).$mount().$el);
 
 			return false;
-		};
-
-		this.selectLocalFile = () => {
-			this.$refs.fileInput.click();
-		};
-
-		this.urlUpload = () => {
+		},
+		selectLocalFile() {
+			(this.$refs.fileInput as any).click();
+		},
+		urlUpload() {
 			inputDialog('%i18n:desktop.tags.mk-drive-browser.url-upload%',
 				'%i18n:desktop.tags.mk-drive-browser.url-of-file%', null, url => {
 
-				this.api('drive/files/upload_from_url', {
+				this.$root.$data.os.api('drive/files/upload_from_url', {
 					url: url,
 					folder_id: this.folder ? this.folder.id : undefined
 				});
@@ -499,34 +304,29 @@
 					text: '%i18n:common.ok%'
 				}]);
 			});
-		};
-
-		this.createFolder = () => {
+		},
+		createFolder() {
 			inputDialog('%i18n:desktop.tags.mk-drive-browser.create-folder%',
 				'%i18n:desktop.tags.mk-drive-browser.folder-name%', null, name => {
 
-				this.api('drive/folders/create', {
+				this.$root.$data.os.api('drive/folders/create', {
 					name: name,
 					folder_id: this.folder ? this.folder.id : undefined
 				}).then(folder => {
 					this.addFolder(folder, true);
-					this.update();
 				});
 			});
-		};
-
-		this.changeFileInput = () => {
-			Array.from(this.$refs.fileInput.files).forEach(file => {
+		},
+		onChangeFileInput() {
+			Array.from((this.$refs.fileInput as any).files).forEach(file => {
 				this.upload(file, this.folder);
 			});
-		};
-
-		this.upload = (file, folder) => {
+		},
+		upload(file, folder) {
 			if (folder && typeof folder == 'object') folder = folder.id;
-			this.$refs.uploader.upload(file, folder);
-		};
-
-		this.chooseFile = file => {
+			(this.$refs.uploader as any).upload(file, folder);
+		},
+		chooseFile(file) {
 			const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id);
 			if (this.multiple) {
 				if (isAlreadySelected) {
@@ -534,7 +334,6 @@
 				} else {
 					this.selectedFiles.push(file);
 				}
-				this.update();
 				this.$emit('change-selection', this.selectedFiles);
 			} else {
 				if (isAlreadySelected) {
@@ -544,15 +343,15 @@
 					this.$emit('change-selection', [file]);
 				}
 			}
-		};
-
-		this.newWindow = folderId => {
-			riot.mount(document.body.appendChild(document.createElement('mk-drive-browser-window')), {
-				folder: folderId
-			});
-		};
-
-		this.move = target => {
+		},
+		newWindow(folderId) {
+			document.body.appendChild(new MkDriveWindow({
+				propsData: {
+					folder: folderId
+				}
+			}).$mount().$el);
+		},
+		move(target) {
 			if (target == null) {
 				this.goRoot();
 				return;
@@ -560,11 +359,9 @@
 				target = target.id;
 			}
 
-			this.update({
-				fetching: true
-			});
+			this.fetching = true;
 
-			this.api('drive/folders/show', {
+			this.$root.$data.os.api('drive/folders/show', {
 				folder_id: target
 			}).then(folder => {
 				this.folder = folder;
@@ -577,20 +374,17 @@
 
 				if (folder.parent) dive(folder.parent);
 
-				this.update();
 				this.$emit('open-folder', folder);
 				this.fetch();
 			});
-		};
-
-		this.addFolder = (folder, unshift = false) => {
+		},
+		addFolder(folder, unshift = false) {
 			const current = this.folder ? this.folder.id : null;
 			if (current != folder.parent_id) return;
 
 			if (this.folders.some(f => f.id == folder.id)) {
 				const exist = this.folders.map(f => f.id).indexOf(folder.id);
-				this.folders[exist] = folder;
-				this.update();
+				this.folders[exist] = folder; // TODO
 				return;
 			}
 
@@ -599,18 +393,14 @@
 			} else {
 				this.folders.push(folder);
 			}
-
-			this.update();
-		};
-
-		this.addFile = (file, unshift = false) => {
+		},
+		addFile(file, unshift = false) {
 			const current = this.folder ? this.folder.id : null;
 			if (current != file.folder_id) return;
 
 			if (this.files.some(f => f.id == file.id)) {
 				const exist = this.files.map(f => f.id).indexOf(file.id);
-				this.files[exist] = file;
-				this.update();
+				this.files[exist] = file; // TODO
 				return;
 			}
 
@@ -619,47 +409,42 @@
 			} else {
 				this.files.push(file);
 			}
-
-			this.update();
-		};
-
-		this.removeFolder = folder => {
+		},
+		removeFolder(folder) {
 			if (typeof folder == 'object') folder = folder.id;
 			this.folders = this.folders.filter(f => f.id != folder);
-			this.update();
-		};
-
-		this.removeFile = file => {
+		},
+		removeFile(file) {
 			if (typeof file == 'object') file = file.id;
 			this.files = this.files.filter(f => f.id != file);
-			this.update();
-		};
-
-		this.appendFile = file => this.addFile(file);
-		this.appendFolder = file => this.addFolder(file);
-		this.prependFile = file => this.addFile(file, true);
-		this.prependFolder = file => this.addFolder(file, true);
-
-		this.goRoot = () => {
+		},
+		appendFile(file) {
+			this.addFile(file);
+		},
+		appendFolder(folder) {
+			this.addFolder(folder);
+		},
+		prependFile(file) {
+			this.addFile(file, true);
+		},
+		prependFolder(folder) {
+			this.addFolder(folder, true);
+		},
+		goRoot() {
 			// 既にrootにいるなら何もしない
 			if (this.folder == null) return;
 
-			this.update({
-				folder: null,
-				hierarchyFolders: []
-			});
+			this.folder = null;
+			this.hierarchyFolders = [];
 			this.$emit('move-root');
 			this.fetch();
-		};
-
-		this.fetch = () => {
-			this.update({
-				folders: [],
-				files: [],
-				moreFolders: false,
-				moreFiles: false,
-				fetching: true
-			});
+		},
+		fetch() {
+			this.folders = [];
+			this.files = [];
+			this.moreFolders = false;
+			this.moreFiles = false;
+			this.fetching = true;
 
 			let fetchedFolders = null;
 			let fetchedFiles = null;
@@ -668,7 +453,7 @@
 			const filesMax = 30;
 
 			// フォルダ一覧取得
-			this.api('drive/folders', {
+			this.$root.$data.os.api('drive/folders', {
 				folder_id: this.folder ? this.folder.id : null,
 				limit: foldersMax + 1
 			}).then(folders => {
@@ -681,7 +466,7 @@
 			});
 
 			// ファイル一覧取得
-			this.api('drive/files', {
+			this.$root.$data.os.api('drive/files', {
 				folder_id: this.folder ? this.folder.id : null,
 				limit: filesMax + 1
 			}).then(files => {
@@ -698,24 +483,19 @@
 				if (flag) {
 					fetchedFolders.forEach(this.appendFolder);
 					fetchedFiles.forEach(this.appendFile);
-					this.update({
-						fetching: false
-					});
+					this.fetching = false;
 				} else {
 					flag = true;
 				}
 			};
-		};
-
-		this.fetchMoreFiles = () => {
-			this.update({
-				fetching: true
-			});
+		},
+		fetchMoreFiles() {
+			this.fetching = true;
 
 			const max = 30;
 
 			// ファイル一覧取得
-			this.api('drive/files', {
+			this.$root.$data.os.api('drive/files', {
 				folder_id: this.folder ? this.folder.id : null,
 				limit: max + 1
 			}).then(files => {
@@ -726,11 +506,205 @@
 					this.moreFiles = false;
 				}
 				files.forEach(this.appendFile);
-				this.update({
-					fetching: false
-				});
+				this.fetching = false;
 			});
-		};
+		}
+	}
+});
+</script>
 
-	</script>
-</mk-drive-browser>
+<style lang="stylus" scoped>
+.mk-drive
+
+	> nav
+		display block
+		z-index 2
+		width 100%
+		overflow auto
+		font-size 0.9em
+		color #555
+		background #fff
+		//border-bottom 1px solid #dfdfdf
+		box-shadow 0 1px 0 rgba(0, 0, 0, 0.05)
+
+		&, *
+			user-select none
+
+		> .path
+			display inline-block
+			vertical-align bottom
+			margin 0
+			padding 0 8px
+			width calc(100% - 200px)
+			line-height 38px
+			white-space nowrap
+
+			> *
+				display inline-block
+				margin 0
+				padding 0 8px
+				line-height 38px
+				cursor pointer
+
+				i
+					margin-right 4px
+
+				*
+					pointer-events none
+
+				&:hover
+					text-decoration underline
+
+				&.current
+					font-weight bold
+					cursor default
+
+					&:hover
+						text-decoration none
+
+				&.separator
+					margin 0
+					padding 0
+					opacity 0.5
+					cursor default
+
+					> [data-fa]
+						margin 0
+
+		> .search
+			display inline-block
+			vertical-align bottom
+			user-select text
+			cursor auto
+			margin 0
+			padding 0 18px
+			width 200px
+			font-size 1em
+			line-height 38px
+			background transparent
+			outline none
+			//border solid 1px #ddd
+			border none
+			border-radius 0
+			box-shadow none
+			transition color 0.5s ease, border 0.5s ease
+			font-family FontAwesome, sans-serif
+
+			&[data-active='true']
+				background #fff
+
+			&::-webkit-input-placeholder,
+			&:-ms-input-placeholder,
+			&:-moz-placeholder
+				color $ui-control-foreground-color
+
+	> .main
+		padding 8px
+		height calc(100% - 38px)
+		overflow auto
+
+		&, *
+			user-select none
+
+		&.fetching
+			cursor wait !important
+
+			*
+				pointer-events none
+
+			> .contents
+				opacity 0.5
+
+		&.uploading
+			height calc(100% - 38px - 100px)
+
+		> .selection
+			display none
+			position absolute
+			z-index 128
+			top 0
+			left 0
+			border solid 1px $theme-color
+			background rgba($theme-color, 0.5)
+			pointer-events none
+
+		> .contents
+
+			> .folders
+			> .files
+				display flex
+				flex-wrap wrap
+
+				> .folder
+				> .file
+					flex-grow 1
+					width 144px
+					margin 4px
+
+				> .padding
+					flex-grow 1
+					pointer-events none
+					width 144px + 8px // 8px is margin
+
+			> .empty
+				padding 16px
+				text-align center
+				color #999
+				pointer-events none
+
+				> p
+					margin 0
+
+		> .fetching
+			.spinner
+				margin 100px auto
+				width 40px
+				height 40px
+				text-align center
+
+				animation sk-rotate 2.0s infinite linear
+
+			.dot1, .dot2
+				width 60%
+				height 60%
+				display inline-block
+				position absolute
+				top 0
+				background-color rgba(0, 0, 0, 0.3)
+				border-radius 100%
+
+				animation sk-bounce 2.0s infinite ease-in-out
+
+			.dot2
+				top auto
+				bottom 0
+				animation-delay -1.0s
+
+			@keyframes sk-rotate { 100% { transform: rotate(360deg); }}
+
+			@keyframes sk-bounce {
+				0%, 100% {
+					transform: scale(0.0);
+				} 50% {
+					transform: scale(1.0);
+				}
+			}
+
+	> .dropzone
+		position absolute
+		left 0
+		top 38px
+		width 100%
+		height calc(100% - 38px)
+		border dashed 2px rgba($theme-color, 0.5)
+		pointer-events none
+
+	> .mk-uploader
+		height 100px
+		padding 16px
+		background #fff
+
+	> input
+		display none
+
+</style>

From 727d2d8737751b8455fd65489859a81f6468e321 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 18:01:15 +0900
Subject: [PATCH 099/286] wip

---
 src/web/app/desktop/-tags/contextmenu.tag     | 138 -----------------
 .../desktop/-tags/drive/base-contextmenu.tag  |  44 ------
 .../desktop/views/components/contextmenu.vue  | 142 ++++++++++++++++++
 .../views/components/drive-contextmenu.vue    |  46 ++++++
 4 files changed, 188 insertions(+), 182 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/contextmenu.tag
 delete mode 100644 src/web/app/desktop/-tags/drive/base-contextmenu.tag
 create mode 100644 src/web/app/desktop/views/components/contextmenu.vue
 create mode 100644 src/web/app/desktop/views/components/drive-contextmenu.vue

diff --git a/src/web/app/desktop/-tags/contextmenu.tag b/src/web/app/desktop/-tags/contextmenu.tag
deleted file mode 100644
index cb9db4f98..000000000
--- a/src/web/app/desktop/-tags/contextmenu.tag
+++ /dev/null
@@ -1,138 +0,0 @@
-<mk-contextmenu>
-	<yield />
-	<style lang="stylus" scoped>
-		:scope
-			$width = 240px
-			$item-height = 38px
-			$padding = 10px
-
-			display none
-			position fixed
-			top 0
-			left 0
-			z-index 4096
-			width $width
-			font-size 0.8em
-			background #fff
-			border-radius 0 4px 4px 4px
-			box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2)
-			opacity 0
-
-			ul
-				display block
-				margin 0
-				padding $padding 0
-				list-style none
-
-			li
-				display block
-
-				&.separator
-					margin-top $padding
-					padding-top $padding
-					border-top solid 1px #eee
-
-				&.has-child
-					> p
-						cursor default
-
-						> [data-fa]:last-child
-							position absolute
-							top 0
-							right 8px
-							line-height $item-height
-
-					&:hover > ul
-						visibility visible
-
-					&:active
-						> p, a
-							background $theme-color
-
-				> p, a
-					display block
-					z-index 1
-					margin 0
-					padding 0 32px 0 38px
-					line-height $item-height
-					color #868C8C
-					text-decoration none
-					cursor pointer
-
-					&:hover
-						text-decoration none
-
-					*
-						pointer-events none
-
-					> i
-						width 28px
-						margin-left -28px
-						text-align center
-
-				&:hover
-					> p, a
-						text-decoration none
-						background $theme-color
-						color $theme-color-foreground
-
-				&:active
-					> p, a
-						text-decoration none
-						background darken($theme-color, 10%)
-						color $theme-color-foreground
-
-			li > ul
-				visibility hidden
-				position absolute
-				top 0
-				left $width
-				margin-top -($padding)
-				width $width
-				background #fff
-				border-radius 0 4px 4px 4px
-				box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2)
-				transition visibility 0s linear 0.2s
-
-	</style>
-	<script lang="typescript">
-		import * as anime from 'animejs';
-		import contains from '../../common/scripts/contains';
-
-		this.root.addEventListener('contextmenu', e => {
-			e.preventDefault();
-		});
-
-		this.mousedown = e => {
-			e.preventDefault();
-			if (!contains(this.root, e.target) && (this.root != e.target)) this.close();
-			return false;
-		};
-
-		this.open = pos => {
-			document.querySelectorAll('body *').forEach(el => {
-				el.addEventListener('mousedown', this.mousedown);
-			});
-
-			this.root.style.display = 'block';
-			this.root.style.left = pos.x + 'px';
-			this.root.style.top = pos.y + 'px';
-
-			anime({
-				targets: this.root,
-				opacity: [0, 1],
-				duration: 100,
-				easing: 'linear'
-			});
-		};
-
-		this.close = () => {
-			document.querySelectorAll('body *').forEach(el => {
-				el.removeEventListener('mousedown', this.mousedown);
-			});
-
-			this.$emit('closed');
-			this.$destroy();
-		};
-	</script>
-</mk-contextmenu>
diff --git a/src/web/app/desktop/-tags/drive/base-contextmenu.tag b/src/web/app/desktop/-tags/drive/base-contextmenu.tag
deleted file mode 100644
index c93d63026..000000000
--- a/src/web/app/desktop/-tags/drive/base-contextmenu.tag
+++ /dev/null
@@ -1,44 +0,0 @@
-<mk-drive-browser-base-contextmenu>
-	<mk-contextmenu ref="ctx">
-		<ul>
-			<li @click="parent.createFolder">
-				<p>%fa:R folder%%i18n:desktop.tags.mk-drive-browser-base-contextmenu.create-folder%</p>
-			</li>
-			<li @click="parent.upload">
-				<p>%fa:upload%%i18n:desktop.tags.mk-drive-browser-base-contextmenu.upload%</p>
-			</li>
-			<li @click="parent.urlUpload">
-				<p>%fa:cloud-upload-alt%%i18n:desktop.tags.mk-drive-browser-base-contextmenu.url-upload%</p>
-			</li>
-		</ul>
-	</mk-contextmenu>
-	<script lang="typescript">
-		this.browser = this.opts.browser;
-
-		this.on('mount', () => {
-			this.$refs.ctx.on('closed', () => {
-				this.$emit('closed');
-				this.$destroy();
-			});
-		});
-
-		this.open = pos => {
-			this.$refs.ctx.open(pos);
-		};
-
-		this.createFolder = () => {
-			this.browser.createFolder();
-			this.$refs.ctx.close();
-		};
-
-		this.upload = () => {
-			this.browser.selectLocalFile();
-			this.$refs.ctx.close();
-		};
-
-		this.urlUpload = () => {
-			this.browser.urlUpload();
-			this.$refs.ctx.close();
-		};
-	</script>
-</mk-drive-browser-base-contextmenu>
diff --git a/src/web/app/desktop/views/components/contextmenu.vue b/src/web/app/desktop/views/components/contextmenu.vue
new file mode 100644
index 000000000..c6fccc22c
--- /dev/null
+++ b/src/web/app/desktop/views/components/contextmenu.vue
@@ -0,0 +1,142 @@
+<template>
+<div class="mk-contextmenu" @contextmenu.prevent="() => {}">
+	<slot></slot>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+import contains from '../../../common/scripts/contains';
+
+export default Vue.extend({
+	props: ['x', 'y'],
+	mounted() {
+		document.querySelectorAll('body *').forEach(el => {
+			el.addEventListener('mousedown', this.onMousedown);
+		});
+
+		this.$el.style.display = 'block';
+		this.$el.style.left = this.x + 'px';
+		this.$el.style.top = this.y + 'px';
+
+		anime({
+			targets: this.$el,
+			opacity: [0, 1],
+			duration: 100,
+			easing: 'linear'
+		});
+	},
+	methods: {
+		onMousedown(e) {
+			e.preventDefault();
+			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
+			return false;
+		},
+		close() {
+			Array.from(document.querySelectorAll('body *')).forEach(el => {
+				el.removeEventListener('mousedown', this.onMousedown);
+			});
+
+			this.$emit('closed');
+			this.$destroy();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-contextmenu
+	$width = 240px
+	$item-height = 38px
+	$padding = 10px
+
+	display none
+	position fixed
+	top 0
+	left 0
+	z-index 4096
+	width $width
+	font-size 0.8em
+	background #fff
+	border-radius 0 4px 4px 4px
+	box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2)
+	opacity 0
+
+	ul
+		display block
+		margin 0
+		padding $padding 0
+		list-style none
+
+	li
+		display block
+
+		&.separator
+			margin-top $padding
+			padding-top $padding
+			border-top solid 1px #eee
+
+		&.has-child
+			> p
+				cursor default
+
+				> [data-fa]:last-child
+					position absolute
+					top 0
+					right 8px
+					line-height $item-height
+
+			&:hover > ul
+				visibility visible
+
+			&:active
+				> p, a
+					background $theme-color
+
+		> p, a
+			display block
+			z-index 1
+			margin 0
+			padding 0 32px 0 38px
+			line-height $item-height
+			color #868C8C
+			text-decoration none
+			cursor pointer
+
+			&:hover
+				text-decoration none
+
+			*
+				pointer-events none
+
+			> i
+				width 28px
+				margin-left -28px
+				text-align center
+
+		&:hover
+			> p, a
+				text-decoration none
+				background $theme-color
+				color $theme-color-foreground
+
+		&:active
+			> p, a
+				text-decoration none
+				background darken($theme-color, 10%)
+				color $theme-color-foreground
+
+	li > ul
+		visibility hidden
+		position absolute
+		top 0
+		left $width
+		margin-top -($padding)
+		width $width
+		background #fff
+		border-radius 0 4px 4px 4px
+		box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2)
+		transition visibility 0s linear 0.2s
+
+</style>
diff --git a/src/web/app/desktop/views/components/drive-contextmenu.vue b/src/web/app/desktop/views/components/drive-contextmenu.vue
new file mode 100644
index 000000000..bdb3bd00d
--- /dev/null
+++ b/src/web/app/desktop/views/components/drive-contextmenu.vue
@@ -0,0 +1,46 @@
+<template>
+<mk-contextmenu ref="menu" @closed="onClosed">
+	<ul>
+		<li @click="createFolder">
+			<p>%fa:R folder%%i18n:desktop.tags.mk-drive-browser-base-contextmenu.create-folder%</p>
+		</li>
+		<li @click="upload">
+			<p>%fa:upload%%i18n:desktop.tags.mk-drive-browser-base-contextmenu.upload%</p>
+		</li>
+		<li @click="urlUpload">
+			<p>%fa:cloud-upload-alt%%i18n:desktop.tags.mk-drive-browser-base-contextmenu.url-upload%</p>
+		</li>
+	</ul>
+</mk-contextmenu>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['browser'],
+	mounted() {
+
+	},
+	methods: {
+		close() {
+			(this.$refs.menu as any).close();
+		},
+		onClosed() {
+			this.$emit('closed');
+			this.$destroy();
+		},
+		createFolder() {
+			this.browser.createFolder();
+			this.close();
+		},
+		upload() {
+			this.browser.selectLocalFile();
+			this.close();
+		},
+		urlUpload() {
+			this.browser.urlUpload();
+			this.close();
+		}
+	}
+});
+</script>

From 9a722898d2a49b935c70f1b1c9dcff11e0464be3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 19:03:48 +0900
Subject: [PATCH 100/286] wip

---
 src/web/app/desktop/-tags/drive/file.tag      | 217 ----------------
 src/web/app/desktop/-tags/drive/folder.tag    | 202 ---------------
 .../desktop/views/components/drive-file.vue   | 232 ++++++++++++++++++
 .../desktop/views/components/drive-folder.vue | 220 +++++++++++++++++
 .../views/components/post-form-window.vue     |   4 +-
 .../desktop/views/components/post-form.vue    |   4 +-
 6 files changed, 457 insertions(+), 422 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/drive/file.tag
 delete mode 100644 src/web/app/desktop/-tags/drive/folder.tag
 create mode 100644 src/web/app/desktop/views/components/drive-file.vue
 create mode 100644 src/web/app/desktop/views/components/drive-folder.vue

diff --git a/src/web/app/desktop/-tags/drive/file.tag b/src/web/app/desktop/-tags/drive/file.tag
deleted file mode 100644
index 153a038f4..000000000
--- a/src/web/app/desktop/-tags/drive/file.tag
+++ /dev/null
@@ -1,217 +0,0 @@
-<mk-drive-browser-file data-is-selected={ isSelected } data-is-contextmenu-showing={ isContextmenuShowing.toString() } @click="onclick" oncontextmenu={ oncontextmenu } draggable="true" ondragstart={ ondragstart } ondragend={ ondragend } title={ title }>
-	<div class="label" v-if="I.avatar_id == file.id"><img src="/assets/label.svg"/>
-		<p>%i18n:desktop.tags.mk-drive-browser-file.avatar%</p>
-	</div>
-	<div class="label" v-if="I.banner_id == file.id"><img src="/assets/label.svg"/>
-		<p>%i18n:desktop.tags.mk-drive-browser-file.banner%</p>
-	</div>
-	<div class="thumbnail" ref="thumbnail" style="background-color:{ file.properties.average_color ? 'rgb(' + file.properties.average_color.join(',') + ')' : 'transparent' }">
-		<img src={ file.url + '?thumbnail&size=128' } alt="" onload={ onload }/>
-	</div>
-	<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" v-if="file.name.lastIndexOf('.') != -1">{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding 8px 0 0 0
-			height 180px
-			border-radius 4px
-
-			&, *
-				cursor pointer
-
-			&:hover
-				background rgba(0, 0, 0, 0.05)
-
-				> .label
-					&:before
-					&:after
-						background #0b65a5
-
-			&:active
-				background rgba(0, 0, 0, 0.1)
-
-				> .label
-					&:before
-					&:after
-						background #0b588c
-
-			&[data-is-selected]
-				background $theme-color
-
-				&:hover
-					background lighten($theme-color, 10%)
-
-				&:active
-					background darken($theme-color, 10%)
-
-				> .label
-					&:before
-					&:after
-						display none
-
-				> .name
-					color $theme-color-foreground
-
-			&[data-is-contextmenu-showing='true']
-				&:after
-					content ""
-					pointer-events none
-					position absolute
-					top -4px
-					right -4px
-					bottom -4px
-					left -4px
-					border 2px dashed rgba($theme-color, 0.3)
-					border-radius 4px
-
-			> .label
-				position absolute
-				top 0
-				left 0
-				pointer-events none
-
-				&:before
-					content ""
-					display block
-					position absolute
-					z-index 1
-					top 0
-					left 57px
-					width 28px
-					height 8px
-					background #0c7ac9
-
-				&:after
-					content ""
-					display block
-					position absolute
-					z-index 1
-					top 57px
-					left 0
-					width 8px
-					height 28px
-					background #0c7ac9
-
-				> img
-					position absolute
-					z-index 2
-					top 0
-					left 0
-
-				> p
-					position absolute
-					z-index 3
-					top 19px
-					left -28px
-					width 120px
-					margin 0
-					text-align center
-					line-height 28px
-					color #fff
-					transform rotate(-45deg)
-
-			> .thumbnail
-				width 128px
-				height 128px
-				margin auto
-
-				> img
-					display block
-					position absolute
-					top 0
-					left 0
-					right 0
-					bottom 0
-					margin auto
-					max-width 128px
-					max-height 128px
-					pointer-events none
-
-			> .name
-				display block
-				margin 4px 0 0 0
-				font-size 0.8em
-				text-align center
-				word-break break-all
-				color #444
-				overflow hidden
-
-				> .ext
-					opacity 0.5
-
-	</style>
-	<script lang="typescript">
-		import * as anime from 'animejs';
-		import bytesToSize from '../../../common/scripts/bytes-to-size';
-
-		this.mixin('i');
-
-		this.file = this.opts.file;
-		this.browser = this.parent;
-		this.title = `${this.file.name}\n${this.file.type} ${bytesToSize(this.file.datasize)}`;
-		this.isContextmenuShowing = false;
-		this.isSelected = this.browser.selectedFiles.some(f => f.id == this.file.id);
-
-		this.browser.on('change-selection', selections => {
-			this.isSelected = selections.some(f => f.id == this.file.id);
-			this.update();
-		});
-
-		this.onclick = () => {
-			this.browser.chooseFile(this.file);
-		};
-
-		this.oncontextmenu = e => {
-			e.preventDefault();
-			e.stopImmediatePropagation();
-
-			this.update({
-				isContextmenuShowing: true
-			});
-			const ctx = riot.mount(document.body.appendChild(document.createElement('mk-drive-browser-file-contextmenu')), {
-				browser: this.browser,
-				file: this.file
-			})[0];
-			ctx.open({
-				x: e.pageX - window.pageXOffset,
-				y: e.pageY - window.pageYOffset
-			});
-			ctx.on('closed', () => {
-				this.update({
-					isContextmenuShowing: false
-				});
-			});
-			return false;
-		};
-
-		this.ondragstart = e => {
-			e.dataTransfer.effectAllowed = 'move';
-			e.dataTransfer.setData('text', JSON.stringify({
-				type: 'file',
-				id: this.file.id,
-				file: this.file
-			}));
-			this.isDragging = true;
-
-			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
-			// (=あなたの子供が、ドラッグを開始しましたよ)
-			this.browser.isDragSource = true;
-		};
-
-		this.ondragend = e => {
-			this.isDragging = false;
-			this.browser.isDragSource = false;
-		};
-
-		this.onload = () => {
-			if (this.file.properties.average_color) {
-				anime({
-					targets: this.$refs.thumbnail,
-					backgroundColor: `rgba(${this.file.properties.average_color.join(',')}, 0)`,
-					duration: 100,
-					easing: 'linear'
-				});
-			}
-		};
-	</script>
-</mk-drive-browser-file>
diff --git a/src/web/app/desktop/-tags/drive/folder.tag b/src/web/app/desktop/-tags/drive/folder.tag
deleted file mode 100644
index ed16bfb0d..000000000
--- a/src/web/app/desktop/-tags/drive/folder.tag
+++ /dev/null
@@ -1,202 +0,0 @@
-<mk-drive-browser-folder data-is-contextmenu-showing={ isContextmenuShowing.toString() } data-draghover={ draghover.toString() } @click="onclick" onmouseover={ onmouseover } onmouseout={ onmouseout } ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop } oncontextmenu={ oncontextmenu } draggable="true" ondragstart={ ondragstart } ondragend={ ondragend } title={ title }>
-	<p class="name"><template v-if="hover">%fa:R folder-open .fw%</template><template v-if="!hover">%fa:R folder .fw%</template>{ folder.name }</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding 8px
-			height 64px
-			background lighten($theme-color, 95%)
-			border-radius 4px
-
-			&, *
-				cursor pointer
-
-			*
-				pointer-events none
-
-			&:hover
-				background lighten($theme-color, 90%)
-
-			&:active
-				background lighten($theme-color, 85%)
-
-			&[data-is-contextmenu-showing='true']
-			&[data-draghover='true']
-				&:after
-					content ""
-					pointer-events none
-					position absolute
-					top -4px
-					right -4px
-					bottom -4px
-					left -4px
-					border 2px dashed rgba($theme-color, 0.3)
-					border-radius 4px
-
-			&[data-draghover='true']
-				background lighten($theme-color, 90%)
-
-			> .name
-				margin 0
-				font-size 0.9em
-				color darken($theme-color, 30%)
-
-				> [data-fa]
-					margin-right 4px
-				  margin-left 2px
-					text-align left
-
-	</style>
-	<script lang="typescript">
-		import dialog from '../../scripts/dialog';
-
-		this.mixin('api');
-
-		this.folder = this.opts.folder;
-		this.browser = this.parent;
-
-		this.title = this.folder.name;
-		this.hover = false;
-		this.draghover = false;
-		this.isContextmenuShowing = false;
-
-		this.onclick = () => {
-			this.browser.move(this.folder);
-		};
-
-		this.onmouseover = () => {
-			this.hover = true;
-		};
-
-		this.onmouseout = () => {
-			this.hover = false
-		};
-
-		this.ondragover = e => {
-			e.preventDefault();
-			e.stopPropagation();
-
-			// 自分自身がドラッグされていない場合
-			if (!this.isDragging) {
-				// ドラッグされてきたものがファイルだったら
-				if (e.dataTransfer.effectAllowed === 'all') {
-					e.dataTransfer.dropEffect = 'copy';
-				} else {
-					e.dataTransfer.dropEffect = 'move';
-				}
-			} else {
-				// 自分自身にはドロップさせない
-				e.dataTransfer.dropEffect = 'none';
-			}
-			return false;
-		};
-
-		this.ondragenter = e => {
-			e.preventDefault();
-			if (!this.isDragging) this.draghover = true;
-		};
-
-		this.ondragleave = () => {
-			this.draghover = false;
-		};
-
-		this.ondrop = e => {
-			e.preventDefault();
-			e.stopPropagation();
-			this.draghover = false;
-
-			// ファイルだったら
-			if (e.dataTransfer.files.length > 0) {
-				Array.from(e.dataTransfer.files).forEach(file => {
-					this.browser.upload(file, this.folder);
-				});
-				return false;
-			};
-
-			// データ取得
-			const data = e.dataTransfer.getData('text');
-			if (data == null) return false;
-
-			// パース
-			// TODO: Validate JSON
-			const obj = JSON.parse(data);
-
-			// (ドライブの)ファイルだったら
-			if (obj.type == 'file') {
-				const file = obj.id;
-				this.browser.removeFile(file);
-				this.api('drive/files/update', {
-					file_id: file,
-					folder_id: this.folder.id
-				});
-			// (ドライブの)フォルダーだったら
-			} else if (obj.type == 'folder') {
-				const folder = obj.id;
-				// 移動先が自分自身ならreject
-				if (folder == this.folder.id) return false;
-				this.browser.removeFolder(folder);
-				this.api('drive/folders/update', {
-					folder_id: folder,
-					parent_id: this.folder.id
-				}).then(() => {
-					// something
-				}).catch(err => {
-					switch (err) {
-						case 'detected-circular-definition':
-							dialog('%fa:exclamation-triangle%%i18n:desktop.tags.mk-drive-browser-folder.unable-to-process%',
-								'%i18n:desktop.tags.mk-drive-browser-folder.circular-reference-detected%', [{
-								text: '%i18n:common.ok%'
-							}]);
-							break;
-						default:
-							alert('%i18n:desktop.tags.mk-drive-browser-folder.unhandled-error% ' + err);
-					}
-				});
-			}
-
-			return false;
-		};
-
-		this.ondragstart = e => {
-			e.dataTransfer.effectAllowed = 'move';
-			e.dataTransfer.setData('text', JSON.stringify({
-				type: 'folder',
-				id: this.folder.id
-			}));
-			this.isDragging = true;
-
-			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
-			// (=あなたの子供が、ドラッグを開始しましたよ)
-			this.browser.isDragSource = true;
-		};
-
-		this.ondragend = e => {
-			this.isDragging = false;
-			this.browser.isDragSource = false;
-		};
-
-		this.oncontextmenu = e => {
-			e.preventDefault();
-			e.stopImmediatePropagation();
-
-			this.update({
-				isContextmenuShowing: true
-			});
-			const ctx = riot.mount(document.body.appendChild(document.createElement('mk-drive-browser-folder-contextmenu')), {
-				browser: this.browser,
-				folder: this.folder
-			})[0];
-			ctx.open({
-				x: e.pageX - window.pageXOffset,
-				y: e.pageY - window.pageYOffset
-			});
-			ctx.on('closed', () => {
-				this.update({
-					isContextmenuShowing: false
-				});
-			});
-
-			return false;
-		};
-	</script>
-</mk-drive-browser-folder>
diff --git a/src/web/app/desktop/views/components/drive-file.vue b/src/web/app/desktop/views/components/drive-file.vue
new file mode 100644
index 000000000..cda561d31
--- /dev/null
+++ b/src/web/app/desktop/views/components/drive-file.vue
@@ -0,0 +1,232 @@
+<template>
+<div class="mk-drive-file"
+	:data-is-selected="isSelected"
+	:data-is-contextmenu-showing="isContextmenuShowing"
+	@click="onClick"
+	@contextmenu.prevent.stop="onContextmenu"
+	draggable="true"
+	@dragstart="onDragstart"
+	@dragend="onDragend"
+	:title="title"
+>
+	<div class="label" v-if="I.avatar_id == file.id"><img src="/assets/label.svg"/>
+		<p>%i18n:desktop.tags.mk-drive-browser-file.avatar%</p>
+	</div>
+	<div class="label" v-if="I.banner_id == file.id"><img src="/assets/label.svg"/>
+		<p>%i18n:desktop.tags.mk-drive-browser-file.banner%</p>
+	</div>
+	<div class="thumbnail" ref="thumbnail" style="background-color:{ file.properties.average_color ? 'rgb(' + file.properties.average_color.join(',') + ')' : 'transparent' }">
+		<img src={ file.url + '?thumbnail&size=128' } alt="" @load="onThumbnailLoaded"/>
+	</div>
+	<p class="name">
+		<span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span>
+		<span class="ext" v-if="file.name.lastIndexOf('.') != -1">{ file.name.substr(file.name.lastIndexOf('.')) }</span>
+	</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+import bytesToSize from '../../../common/scripts/bytes-to-size';
+
+export default Vue.extend({
+	props: ['file', 'browser'],
+	data() {
+		return {
+			isContextmenuShowing: false,
+			isDragging: false
+		};
+	},
+	computed: {
+		isSelected(): boolean {
+			return this.browser.selectedFiles.some(f => f.id == this.file.id);
+		},
+		title(): string {
+			return `${this.file.name}\n${this.file.type} ${bytesToSize(this.file.datasize)}`;
+		}
+	},
+	methods: {
+		onClick() {
+			this.browser.chooseFile(this.file);
+		},
+
+		onContextmenu(e) {
+			this.isContextmenuShowing = true;
+			const ctx = new MkDriveFileContextmenu({
+				parent: this,
+				propsData: {
+					browser: this.browser,
+					x: e.pageX - window.pageXOffset,
+					y: e.pageY - window.pageYOffset
+				}
+			}).$mount();
+			ctx.$once('closed', () => {
+				this.isContextmenuShowing = false;
+			});
+			document.body.appendChild(ctx.$el);
+		},
+
+		onDragstart(e) {
+			e.dataTransfer.effectAllowed = 'move';
+			e.dataTransfer.setData('text', JSON.stringify({
+				type: 'file',
+				id: this.file.id,
+				file: this.file
+			}));
+			this.isDragging = true;
+
+			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
+			// (=あなたの子供が、ドラッグを開始しましたよ)
+			this.browser.isDragSource = true;
+		},
+
+		onDragend(e) {
+			this.isDragging = false;
+			this.browser.isDragSource = false;
+		},
+
+		onThumbnailLoaded() {
+			if (this.file.properties.average_color) {
+				anime({
+					targets: this.$refs.thumbnail,
+					backgroundColor: `rgba(${this.file.properties.average_color.join(',')}, 0)`,
+					duration: 100,
+					easing: 'linear'
+				});
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-drive-file
+	padding 8px 0 0 0
+	height 180px
+	border-radius 4px
+
+	&, *
+		cursor pointer
+
+	&:hover
+		background rgba(0, 0, 0, 0.05)
+
+		> .label
+			&:before
+			&:after
+				background #0b65a5
+
+	&:active
+		background rgba(0, 0, 0, 0.1)
+
+		> .label
+			&:before
+			&:after
+				background #0b588c
+
+	&[data-is-selected]
+		background $theme-color
+
+		&:hover
+			background lighten($theme-color, 10%)
+
+		&:active
+			background darken($theme-color, 10%)
+
+		> .label
+			&:before
+			&:after
+				display none
+
+		> .name
+			color $theme-color-foreground
+
+	&[data-is-contextmenu-showing]
+		&:after
+			content ""
+			pointer-events none
+			position absolute
+			top -4px
+			right -4px
+			bottom -4px
+			left -4px
+			border 2px dashed rgba($theme-color, 0.3)
+			border-radius 4px
+
+	> .label
+		position absolute
+		top 0
+		left 0
+		pointer-events none
+
+		&:before
+			content ""
+			display block
+			position absolute
+			z-index 1
+			top 0
+			left 57px
+			width 28px
+			height 8px
+			background #0c7ac9
+
+		&:after
+			content ""
+			display block
+			position absolute
+			z-index 1
+			top 57px
+			left 0
+			width 8px
+			height 28px
+			background #0c7ac9
+
+		> img
+			position absolute
+			z-index 2
+			top 0
+			left 0
+
+		> p
+			position absolute
+			z-index 3
+			top 19px
+			left -28px
+			width 120px
+			margin 0
+			text-align center
+			line-height 28px
+			color #fff
+			transform rotate(-45deg)
+
+	> .thumbnail
+		width 128px
+		height 128px
+		margin auto
+
+		> img
+			display block
+			position absolute
+			top 0
+			left 0
+			right 0
+			bottom 0
+			margin auto
+			max-width 128px
+			max-height 128px
+			pointer-events none
+
+	> .name
+		display block
+		margin 4px 0 0 0
+		font-size 0.8em
+		text-align center
+		word-break break-all
+		color #444
+		overflow hidden
+
+		> .ext
+			opacity 0.5
+
+</style>
diff --git a/src/web/app/desktop/views/components/drive-folder.vue b/src/web/app/desktop/views/components/drive-folder.vue
new file mode 100644
index 000000000..e9e4f1de2
--- /dev/null
+++ b/src/web/app/desktop/views/components/drive-folder.vue
@@ -0,0 +1,220 @@
+<template>
+<div class="mk-drive-folder"
+	:data-is-contextmenu-showing="isContextmenuShowing"
+	:data-draghover="draghover"
+	@click="onClick"
+	@mouseover="onMouseover"
+	@mouseout="onMouseout"
+	@dragover.prevent.stop="onDragover"
+	@dragenter.prevent="onDragenter"
+	@dragleave="onDragleave"
+	@drop.prevent.stop="onDrop"
+	@contextmenu.prevent.stop="onContextmenu"
+	draggable="true"
+	@dragstart="onDragstart"
+	@dragend="onDragend"
+	:title="title"
+>
+	<p class="name">
+		<template v-if="hover">%fa:R folder-open .fw%</template>
+		<template v-if="!hover">%fa:R folder .fw%</template>
+		{{ folder.name }}
+	</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dialog from '../../scripts/dialog';
+
+export default Vue.extend({
+	props: ['folder', 'browser'],
+	data() {
+		return {
+			hover: false,
+			draghover: false,
+			isDragging: false,
+			isContextmenuShowing: false
+		};
+	},
+	computed: {
+		title(): string {
+			return this.folder.name;
+		}
+	},
+	methods: {
+		onClick() {
+			this.browser.move(this.folder);
+		},
+
+		onMouseover() {
+			this.hover = true;
+		},
+
+		onMouseout() {
+			this.hover = false
+		},
+
+		onDragover(e) {
+			// 自分自身がドラッグされていない場合
+			if (!this.isDragging) {
+				// ドラッグされてきたものがファイルだったら
+				if (e.dataTransfer.effectAllowed === 'all') {
+					e.dataTransfer.dropEffect = 'copy';
+				} else {
+					e.dataTransfer.dropEffect = 'move';
+				}
+			} else {
+				// 自分自身にはドロップさせない
+				e.dataTransfer.dropEffect = 'none';
+			}
+			return false;
+		},
+
+		onDragenter() {
+			if (!this.isDragging) this.draghover = true;
+		},
+
+		onDragleave() {
+			this.draghover = false;
+		},
+
+		onDrop(e) {
+			this.draghover = false;
+
+			// ファイルだったら
+			if (e.dataTransfer.files.length > 0) {
+				Array.from(e.dataTransfer.files).forEach(file => {
+					this.browser.upload(file, this.folder);
+				});
+				return false;
+			};
+
+			// データ取得
+			const data = e.dataTransfer.getData('text');
+			if (data == null) return false;
+
+			// パース
+			// TODO: Validate JSON
+			const obj = JSON.parse(data);
+
+			// (ドライブの)ファイルだったら
+			if (obj.type == 'file') {
+				const file = obj.id;
+				this.browser.removeFile(file);
+				this.$root.$data.os.api('drive/files/update', {
+					file_id: file,
+					folder_id: this.folder.id
+				});
+			// (ドライブの)フォルダーだったら
+			} else if (obj.type == 'folder') {
+				const folder = obj.id;
+				// 移動先が自分自身ならreject
+				if (folder == this.folder.id) return false;
+				this.browser.removeFolder(folder);
+				this.$root.$data.os.api('drive/folders/update', {
+					folder_id: folder,
+					parent_id: this.folder.id
+				}).then(() => {
+					// something
+				}).catch(err => {
+					switch (err) {
+						case 'detected-circular-definition':
+							dialog('%fa:exclamation-triangle%%i18n:desktop.tags.mk-drive-browser-folder.unable-to-process%',
+								'%i18n:desktop.tags.mk-drive-browser-folder.circular-reference-detected%', [{
+								text: '%i18n:common.ok%'
+							}]);
+							break;
+						default:
+							alert('%i18n:desktop.tags.mk-drive-browser-folder.unhandled-error% ' + err);
+					}
+				});
+			}
+
+			return false;
+		},
+
+		onDragstart(e) {
+			e.dataTransfer.effectAllowed = 'move';
+			e.dataTransfer.setData('text', JSON.stringify({
+				type: 'folder',
+				id: this.folder.id
+			}));
+			this.isDragging = true;
+
+			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
+			// (=あなたの子供が、ドラッグを開始しましたよ)
+			this.browser.isDragSource = true;
+		},
+
+		onDragend() {
+			this.isDragging = false;
+			this.browser.isDragSource = false;
+		},
+
+		onContextmenu(e) {
+			this.isContextmenuShowing = true;
+			const ctx = new MkDriveFolderContextmenu({
+				parent: this,
+				propsData: {
+					browser: this.browser,
+					x: e.pageX - window.pageXOffset,
+					y: e.pageY - window.pageYOffset
+				}
+			}).$mount();
+			ctx.$once('closed', () => {
+				this.isContextmenuShowing = false;
+			});
+			document.body.appendChild(ctx.$el);
+			return false;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-drive-folder
+	padding 8px
+	height 64px
+	background lighten($theme-color, 95%)
+	border-radius 4px
+
+	&, *
+		cursor pointer
+
+	*
+		pointer-events none
+
+	&:hover
+		background lighten($theme-color, 90%)
+
+	&:active
+		background lighten($theme-color, 85%)
+
+	&[data-is-contextmenu-showing]
+	&[data-draghover]
+		&:after
+			content ""
+			pointer-events none
+			position absolute
+			top -4px
+			right -4px
+			bottom -4px
+			left -4px
+			border 2px dashed rgba($theme-color, 0.3)
+			border-radius 4px
+
+	&[data-draghover]
+		background lighten($theme-color, 90%)
+
+	> .name
+		margin 0
+		font-size 0.9em
+		color darken($theme-color, 30%)
+
+		> [data-fa]
+			margin-right 4px
+			margin-left 2px
+			text-align left
+
+</style>
diff --git a/src/web/app/desktop/views/components/post-form-window.vue b/src/web/app/desktop/views/components/post-form-window.vue
index 90e694c92..39e89ca44 100644
--- a/src/web/app/desktop/views/components/post-form-window.vue
+++ b/src/web/app/desktop/views/components/post-form-window.vue
@@ -29,7 +29,9 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		(this.$refs.form as any).focus();
+		Vue.nextTick(() => {
+			(this.$refs.form as any).focus();
+		});
 	},
 	methods: {
 		onChangeUploadings(media) {
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 9efca5ddc..c062c57e1 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -111,7 +111,7 @@ export default Vue.extend({
 		chooseFile() {
 			(this.$refs.file as any).click();
 		},
-		chooseFileFromDrive() {
+		chooseFileFromDrive() {/*
 			const w = new MkDriveFileSelectorWindow({
 				propsData: {
 					multiple: true
@@ -122,7 +122,7 @@ export default Vue.extend({
 
 			w.$once('selected', files => {
 				files.forEach(this.attachMedia);
-			});
+			});*/
 		},
 		attachMedia(driveFile) {
 			this.files.push(driveFile);

From 5d05a25ae2544acebe809bc16523d8611bf807e0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 19:21:15 +0900
Subject: [PATCH 101/286] wip

---
 .../components/drive-nav-folder.vue}          | 97 ++++++++++---------
 .../views/components/post-form-window.vue     |  6 +-
 .../desktop/views/components/post-form.vue    |  2 +-
 3 files changed, 56 insertions(+), 49 deletions(-)
 rename src/web/app/desktop/{-tags/drive/nav-folder.tag => views/components/drive-nav-folder.vue} (60%)

diff --git a/src/web/app/desktop/-tags/drive/nav-folder.tag b/src/web/app/desktop/views/components/drive-nav-folder.vue
similarity index 60%
rename from src/web/app/desktop/-tags/drive/nav-folder.tag
rename to src/web/app/desktop/views/components/drive-nav-folder.vue
index 4bca80f68..556c64f11 100644
--- a/src/web/app/desktop/-tags/drive/nav-folder.tag
+++ b/src/web/app/desktop/views/components/drive-nav-folder.vue
@@ -1,35 +1,38 @@
-<mk-drive-browser-nav-folder data-draghover={ draghover } @click="onclick" ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop }>
-	<template v-if="folder == null">%fa:cloud%</template><span>{ folder == null ? '%i18n:desktop.tags.mk-drive-browser-nav-folder.drive%' : folder.name }</span>
-	<style lang="stylus" scoped>
-		:scope
-			&[data-draghover]
-				background #eee
+<template>
+<div class="mk-drive-nav-folder"
+	:data-draghover="draghover"
+	@click="onClick"
+	@dragover.prevent.stop="onDragover"
+	@dragenter="onDragenter"
+	@dragleave="onDragleave"
+	@drop.stop="onDrop"
+>
+	<template v-if="folder == null">%fa:cloud%</template>
+	<span>{{ folder == null ? '%i18n:desktop.tags.mk-drive-browser-nav-folder.drive%' : folder.name }}</span>
+</div>
+</template>
 
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.folder = this.opts.folder ? this.opts.folder : null;
-		this.browser = this.parent;
-
-		this.hover = false;
-
-		this.onclick = () => {
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['folder', 'browser'],
+	data() {
+		return {
+			hover: false,
+			draghover: false
+		};
+	},
+	methods: {
+		onClick() {
 			this.browser.move(this.folder);
-		};
-
-		this.onmouseover = () => {
-			this.hover = true
-		};
-
-		this.onmouseout = () => {
-			this.hover = false
-		};
-
-		this.ondragover = e => {
-			e.preventDefault();
-			e.stopPropagation();
-
+		},
+		onMouseover() {
+			this.hover = true;
+		},
+		onMouseout() {
+			this.hover = false;
+		},
+		onDragover(e) {
 			// このフォルダがルートかつカレントディレクトリならドロップ禁止
 			if (this.folder == null && this.browser.folder == null) {
 				e.dataTransfer.dropEffect = 'none';
@@ -40,18 +43,14 @@
 				e.dataTransfer.dropEffect = 'move';
 			}
 			return false;
-		};
-
-		this.ondragenter = () => {
+		},
+		onDragenter() {
 			if (this.folder || this.browser.folder) this.draghover = true;
-		};
-
-		this.ondragleave = () => {
+		},
+		onDragleave() {
 			if (this.folder || this.browser.folder) this.draghover = false;
-		};
-
-		this.ondrop = e => {
-			e.stopPropagation();
+		},
+		onDrop(e) {
 			this.draghover = false;
 
 			// ファイルだったら
@@ -74,7 +73,7 @@
 			if (obj.type == 'file') {
 				const file = obj.id;
 				this.browser.removeFile(file);
-				this.api('drive/files/update', {
+				this.$root.$data.os.api('drive/files/update', {
 					file_id: file,
 					folder_id: this.folder ? this.folder.id : null
 				});
@@ -84,13 +83,21 @@
 				// 移動先が自分自身ならreject
 				if (this.folder && folder == this.folder.id) return false;
 				this.browser.removeFolder(folder);
-				this.api('drive/folders/update', {
+				this.$root.$data.os.api('drive/folders/update', {
 					folder_id: folder,
 					parent_id: this.folder ? this.folder.id : null
 				});
 			}
 
 			return false;
-		};
-	</script>
-</mk-drive-browser-nav-folder>
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-drive-nav-folder
+	&[data-draghover]
+		background #eee
+
+</style>
diff --git a/src/web/app/desktop/views/components/post-form-window.vue b/src/web/app/desktop/views/components/post-form-window.vue
index 39e89ca44..dc16d7c9d 100644
--- a/src/web/app/desktop/views/components/post-form-window.vue
+++ b/src/web/app/desktop/views/components/post-form-window.vue
@@ -1,13 +1,13 @@
 <template>
 <mk-window ref="window" is-modal @closed="$destroy">
 	<span slot="header">
-		<span v-if="!parent.opts.reply">%i18n:desktop.tags.mk-post-form-window.post%</span>
-		<span v-if="parent.opts.reply">%i18n:desktop.tags.mk-post-form-window.reply%</span>
+		<span v-if="!reply">%i18n:desktop.tags.mk-post-form-window.post%</span>
+		<span v-if="reply">%i18n:desktop.tags.mk-post-form-window.reply%</span>
 		<span :class="$style.count" v-if="media.length != 0">{{ '%i18n:desktop.tags.mk-post-form-window.attaches%'.replace('{}', media.length) }}</span>
 		<span :class="$style.count" v-if="uploadings.length != 0">{{ '%i18n:desktop.tags.mk-post-form-window.uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
 	</span>
 	<div slot="content">
-		<mk-post-preview v-if="parent.opts.reply" :class="$style.postPreview" :post="reply"/>
+		<mk-post-preview v-if="reply" :class="$style.postPreview" :post="reply"/>
 		<mk-post-form ref="form"
 			:reply="reply"
 			@posted="$refs.window.close"
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index c062c57e1..91ceb5227 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -26,7 +26,7 @@
 	<button ref="drive" title="%i18n:desktop.tags.mk-post-form.attach-media-from-drive%" @click="selectFileFromDrive">%fa:cloud%</button>
 	<button class="kao" title="%i18n:desktop.tags.mk-post-form.insert-a-kao%" @click="kao">%fa:R smile%</button>
 	<button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" @click="poll = true">%fa:chart-pie%</button>
-	<p class="text-count { over: refs.text.value.length > 1000 }">{ '%i18n:desktop.tags.mk-post-form.text-remain%'.replace('{}', 1000 - refs.text.value.length) }</p>
+	<p class="text-count" :class="{ over: text.length > 1000 }">{{ '%i18n:desktop.tags.mk-post-form.text-remain%'.replace('{}', 1000 - text.length) }}</p>
 	<button :class="{ posting }" ref="submit" :disabled="!canPost" @click="post">
 		{{ posting ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }}<mk-ellipsis v-if="posting"/>
 	</button>

From 205e72802572fb1f85aa52ce0cbbfab8576e7e4f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 19:30:35 +0900
Subject: [PATCH 102/286] wip

---
 src/web/app/desktop/views/components/post-form-window.vue | 5 ++++-
 src/web/app/desktop/views/components/ui.vue               | 4 +++-
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/src/web/app/desktop/views/components/post-form-window.vue b/src/web/app/desktop/views/components/post-form-window.vue
index dc16d7c9d..77b47e20a 100644
--- a/src/web/app/desktop/views/components/post-form-window.vue
+++ b/src/web/app/desktop/views/components/post-form-window.vue
@@ -10,7 +10,7 @@
 		<mk-post-preview v-if="reply" :class="$style.postPreview" :post="reply"/>
 		<mk-post-form ref="form"
 			:reply="reply"
-			@posted="$refs.window.close"
+			@posted="onPosted"
 			@change-uploadings="onChangeUploadings"
 			@change-attached-media="onChangeMedia"/>
 	</div>
@@ -39,6 +39,9 @@ export default Vue.extend({
 		},
 		onChangeMedia(media) {
 			this.media = media;
+		},
+		onPosted() {
+			(this.$refs.window as any).close();
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/ui.vue b/src/web/app/desktop/views/components/ui.vue
index 39ec057f8..76851a0f1 100644
--- a/src/web/app/desktop/views/components/ui.vue
+++ b/src/web/app/desktop/views/components/ui.vue
@@ -21,7 +21,9 @@ export default Vue.extend({
 	},
 	methods: {
 		openPostForm() {
-			document.body.appendChild(new MkPostFormWindow().$mount().$el);
+			document.body.appendChild(new MkPostFormWindow({
+				parent: this
+			}).$mount().$el);
 		},
 		onKeydown(e) {
 			if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return;

From 00aaf300c4a8175a0412de93dc9392a647ab5bfa Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 19:33:39 +0900
Subject: [PATCH 103/286] wip

---
 src/web/app/desktop/views/components/window.vue | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 3a7531a6f..414858a1e 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -82,13 +82,15 @@ export default Vue.extend({
 	},
 
 	mounted() {
-		const main = this.$refs.main as any;
-		main.style.top = '15%';
-		main.style.left = (window.innerWidth / 2) - (main.offsetWidth / 2) + 'px';
+		Vue.nextTick(() => {
+			const main = this.$refs.main as any;
+			main.style.top = '15%';
+			main.style.left = (window.innerWidth / 2) - (main.offsetWidth / 2) + 'px';
 
-		window.addEventListener('resize', this.onBrowserResize);
+			window.addEventListener('resize', this.onBrowserResize);
 
-		this.open();
+			this.open();
+		});
 	},
 
 	destroyed() {

From b48b6bde8eba153615ef27a30deba23eb5d18303 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 20:47:40 +0900
Subject: [PATCH 104/286] wip

---
 src/web/app/mobile/tags/user-card.tag  | 55 -----------------------
 src/web/app/mobile/views/user-card.vue | 62 ++++++++++++++++++++++++++
 2 files changed, 62 insertions(+), 55 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/user-card.tag
 create mode 100644 src/web/app/mobile/views/user-card.vue

diff --git a/src/web/app/mobile/tags/user-card.tag b/src/web/app/mobile/tags/user-card.tag
deleted file mode 100644
index 227b8b389..000000000
--- a/src/web/app/mobile/tags/user-card.tag
+++ /dev/null
@@ -1,55 +0,0 @@
-<mk-user-card>
-	<header style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=1024)' : '' }>
-		<a href={ '/' + user.username }>
-			<img src={ user.avatar_url + '?thumbnail&size=200' } alt="avatar"/>
-		</a>
-	</header>
-	<a class="name" href={ '/' + user.username } target="_blank">{ user.name }</a>
-	<p class="username">@{ user.username }</p>
-	<mk-follow-button user={ user }/>
-	<style lang="stylus" scoped>
-		:scope
-			display inline-block
-			width 200px
-			text-align center
-			border-radius 8px
-			background #fff
-
-			> header
-				display block
-				height 80px
-				background-color #ddd
-				background-size cover
-				background-position center
-				border-radius 8px 8px 0 0
-
-				> a
-					> img
-						position absolute
-						top 20px
-						left calc(50% - 40px)
-						width 80px
-						height 80px
-						border solid 2px #fff
-						border-radius 8px
-
-			> .name
-				display block
-				margin 24px 0 0 0
-				font-size 16px
-				color #555
-
-			> .username
-				margin 0
-				font-size 15px
-				color #ccc
-
-			> mk-follow-button
-				display inline-block
-				margin 8px 0 16px 0
-
-	</style>
-	<script lang="typescript">
-		this.user = this.opts.user;
-	</script>
-</mk-user-card>
diff --git a/src/web/app/mobile/views/user-card.vue b/src/web/app/mobile/views/user-card.vue
new file mode 100644
index 000000000..f70def48f
--- /dev/null
+++ b/src/web/app/mobile/views/user-card.vue
@@ -0,0 +1,62 @@
+<template>
+<div class="mk-user-card">
+	<header :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=1024)` : ''">
+		<a :href="`/${user.username}`">
+			<img :src="`${user.avatar_url}?thumbnail&size=200`" alt="avatar"/>
+		</a>
+	</header>
+	<a class="name" :href="`/${user.username}`" target="_blank">{{ user.name }}</a>
+	<p class="username">@{{ user.username }}</p>
+	<mk-follow-button :user="user"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user']
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-card
+	display inline-block
+	width 200px
+	text-align center
+	border-radius 8px
+	background #fff
+
+	> header
+		display block
+		height 80px
+		background-color #ddd
+		background-size cover
+		background-position center
+		border-radius 8px 8px 0 0
+
+		> a
+			> img
+				position absolute
+				top 20px
+				left calc(50% - 40px)
+				width 80px
+				height 80px
+				border solid 2px #fff
+				border-radius 8px
+
+	> .name
+		display block
+		margin 24px 0 0 0
+		font-size 16px
+		color #555
+
+	> .username
+		margin 0
+		font-size 15px
+		color #ccc
+
+	> mk-follow-button
+		display inline-block
+		margin 8px 0 16px 0
+
+</style>

From 484d5c0622bc39546d9dabfb5208c3fd76111ac4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 20:58:48 +0900
Subject: [PATCH 105/286] wip

---
 src/web/app/common/-tags/uploader.tag         | 199 -----------------
 .../app/common/views/components/uploader.vue  | 207 ++++++++++++++++++
 2 files changed, 207 insertions(+), 199 deletions(-)
 delete mode 100644 src/web/app/common/-tags/uploader.tag
 create mode 100644 src/web/app/common/views/components/uploader.vue

diff --git a/src/web/app/common/-tags/uploader.tag b/src/web/app/common/-tags/uploader.tag
deleted file mode 100644
index 519b063fa..000000000
--- a/src/web/app/common/-tags/uploader.tag
+++ /dev/null
@@ -1,199 +0,0 @@
-<mk-uploader>
-	<ol v-if="uploads.length > 0">
-		<li each={ uploads }>
-			<div class="img" style="background-image: url({ img })"></div>
-			<p class="name">%fa:spinner .pulse%{ name }</p>
-			<p class="status"><span class="initing" v-if="progress == undefined">%i18n:common.tags.mk-uploader.waiting%<mk-ellipsis/></span><span class="kb" v-if="progress != undefined">{ String(Math.floor(progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }<i>KB</i> / { String(Math.floor(progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }<i>KB</i></span><span class="percentage" v-if="progress != undefined">{ Math.floor((progress.value / progress.max) * 100) }</span></p>
-			<progress v-if="progress != undefined && progress.value != progress.max" value={ progress.value } max={ progress.max }></progress>
-			<div class="progress initing" v-if="progress == undefined"></div>
-			<div class="progress waiting" v-if="progress != undefined && progress.value == progress.max"></div>
-		</li>
-	</ol>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow auto
-
-			&:empty
-				display none
-
-			> ol
-				display block
-				margin 0
-				padding 0
-				list-style none
-
-				> li
-					display block
-					margin 8px 0 0 0
-					padding 0
-					height 36px
-					box-shadow 0 -1px 0 rgba($theme-color, 0.1)
-					border-top solid 8px transparent
-
-					&:first-child
-						margin 0
-						box-shadow none
-						border-top none
-
-					> .img
-						display block
-						position absolute
-						top 0
-						left 0
-						width 36px
-						height 36px
-						background-size cover
-						background-position center center
-
-					> .name
-						display block
-						position absolute
-						top 0
-						left 44px
-						margin 0
-						padding 0
-						max-width 256px
-						font-size 0.8em
-						color rgba($theme-color, 0.7)
-						white-space nowrap
-						text-overflow ellipsis
-						overflow hidden
-
-						> [data-fa]
-							margin-right 4px
-
-					> .status
-						display block
-						position absolute
-						top 0
-						right 0
-						margin 0
-						padding 0
-						font-size 0.8em
-
-						> .initing
-							color rgba($theme-color, 0.5)
-
-						> .kb
-							color rgba($theme-color, 0.5)
-
-						> .percentage
-							display inline-block
-							width 48px
-							text-align right
-
-							color rgba($theme-color, 0.7)
-
-							&:after
-								content '%'
-
-					> progress
-						display block
-						position absolute
-						bottom 0
-						right 0
-						margin 0
-						width calc(100% - 44px)
-						height 8px
-						background transparent
-						border none
-						border-radius 4px
-						overflow hidden
-
-						&::-webkit-progress-value
-							background $theme-color
-
-						&::-webkit-progress-bar
-							background rgba($theme-color, 0.1)
-
-					> .progress
-						display block
-						position absolute
-						bottom 0
-						right 0
-						margin 0
-						width calc(100% - 44px)
-						height 8px
-						border none
-						border-radius 4px
-						background linear-gradient(
-							45deg,
-							lighten($theme-color, 30%) 25%,
-							$theme-color               25%,
-							$theme-color               50%,
-							lighten($theme-color, 30%) 50%,
-							lighten($theme-color, 30%) 75%,
-							$theme-color               75%,
-							$theme-color
-						)
-						background-size 32px 32px
-						animation bg 1.5s linear infinite
-
-						&.initing
-							opacity 0.3
-
-						@keyframes bg
-							from {background-position: 0 0;}
-							to   {background-position: -64px 32px;}
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-
-		this.uploads = [];
-
-		this.upload = (file, folder) => {
-			if (folder && typeof folder == 'object') folder = folder.id;
-
-			const id = Math.random();
-
-			const ctx = {
-				id: id,
-				name: file.name || 'untitled',
-				progress: undefined
-			};
-
-			this.uploads.push(ctx);
-			this.$emit('change-uploads', this.uploads);
-			this.update();
-
-			const reader = new FileReader();
-			reader.onload = e => {
-				ctx.img = e.target.result;
-				this.update();
-			};
-			reader.readAsDataURL(file);
-
-			const data = new FormData();
-			data.append('i', this.I.token);
-			data.append('file', file);
-
-			if (folder) data.append('folder_id', folder);
-
-			const xhr = new XMLHttpRequest();
-			xhr.open('POST', _API_URL_ + '/drive/files/create', true);
-			xhr.onload = e => {
-				const driveFile = JSON.parse(e.target.response);
-
-				this.$emit('uploaded', driveFile);
-
-				this.uploads = this.uploads.filter(x => x.id != id);
-				this.$emit('change-uploads', this.uploads);
-
-				this.update();
-			};
-
-			xhr.upload.onprogress = e => {
-				if (e.lengthComputable) {
-					if (ctx.progress == undefined) ctx.progress = {};
-					ctx.progress.max = e.total;
-					ctx.progress.value = e.loaded;
-					this.update();
-				}
-			};
-
-			xhr.send(data);
-		};
-	</script>
-</mk-uploader>
diff --git a/src/web/app/common/views/components/uploader.vue b/src/web/app/common/views/components/uploader.vue
new file mode 100644
index 000000000..1239bec55
--- /dev/null
+++ b/src/web/app/common/views/components/uploader.vue
@@ -0,0 +1,207 @@
+<template>
+<div class="mk-uploader">
+	<ol v-if="uploads.length > 0">
+		<li v-for="ctx in uploads" :key="ctx.id">
+			<div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div>
+			<p class="name">%fa:spinner .pulse%{{ ctx.name }}</p>
+			<p class="status">
+				<span class="initing" v-if="ctx.progress == undefined">%i18n:common.tags.mk-uploader.waiting%<mk-ellipsis/></span>
+				<span class="kb" v-if="ctx.progress != undefined">{{ String(Math.floor(ctx.progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }<i>KB</i></span>
+				<span class="percentage" v-if="ctx.progress != undefined">{{ Math.floor((ctx.progress.value / ctx.progress.max) * 100) }}</span>
+			</p>
+			<progress v-if="ctx.progress != undefined && ctx.progress.value != ctx.progress.max" :value="ctx.progress.value" :max="ctx.progress.max"></progress>
+			<div class="progress initing" v-if="ctx.progress == undefined"></div>
+			<div class="progress waiting" v-if="ctx.progress != undefined && ctx.progress.value == ctx.progress.max"></div>
+		</li>
+	</ol>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			uploads: []
+		};
+	},
+	methods: {
+		upload(file, folder) {
+			if (folder && typeof folder == 'object') folder = folder.id;
+
+			const id = Math.random();
+
+			const ctx = {
+				id: id,
+				name: file.name || 'untitled',
+				progress: undefined
+			};
+
+			this.uploads.push(ctx);
+			this.$emit('change', this.uploads);
+
+			const reader = new FileReader();
+			reader.onload = e => {
+				ctx.img = e.target.result;
+			};
+			reader.readAsDataURL(file);
+
+			const data = new FormData();
+			data.append('i', this.$root.$data.os.i.token);
+			data.append('file', file);
+
+			if (folder) data.append('folder_id', folder);
+
+			const xhr = new XMLHttpRequest();
+			xhr.open('POST', _API_URL_ + '/drive/files/create', true);
+			xhr.onload = e => {
+				const driveFile = JSON.parse(e.target.response);
+
+				this.$emit('uploaded', driveFile);
+
+				this.uploads = this.uploads.filter(x => x.id != id);
+				this.$emit('change', this.uploads);
+			};
+
+			xhr.upload.onprogress = e => {
+				if (e.lengthComputable) {
+					if (ctx.progress == undefined) ctx.progress = {};
+					ctx.progress.max = e.total;
+					ctx.progress.value = e.loaded;
+				}
+			};
+
+			xhr.send(data);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-uploader
+	overflow auto
+
+	&:empty
+		display none
+
+	> ol
+		display block
+		margin 0
+		padding 0
+		list-style none
+
+		> li
+			display block
+			margin 8px 0 0 0
+			padding 0
+			height 36px
+			box-shadow 0 -1px 0 rgba($theme-color, 0.1)
+			border-top solid 8px transparent
+
+			&:first-child
+				margin 0
+				box-shadow none
+				border-top none
+
+			> .img
+				display block
+				position absolute
+				top 0
+				left 0
+				width 36px
+				height 36px
+				background-size cover
+				background-position center center
+
+			> .name
+				display block
+				position absolute
+				top 0
+				left 44px
+				margin 0
+				padding 0
+				max-width 256px
+				font-size 0.8em
+				color rgba($theme-color, 0.7)
+				white-space nowrap
+				text-overflow ellipsis
+				overflow hidden
+
+				> [data-fa]
+					margin-right 4px
+
+			> .status
+				display block
+				position absolute
+				top 0
+				right 0
+				margin 0
+				padding 0
+				font-size 0.8em
+
+				> .initing
+					color rgba($theme-color, 0.5)
+
+				> .kb
+					color rgba($theme-color, 0.5)
+
+				> .percentage
+					display inline-block
+					width 48px
+					text-align right
+
+					color rgba($theme-color, 0.7)
+
+					&:after
+						content '%'
+
+			> progress
+				display block
+				position absolute
+				bottom 0
+				right 0
+				margin 0
+				width calc(100% - 44px)
+				height 8px
+				background transparent
+				border none
+				border-radius 4px
+				overflow hidden
+
+				&::-webkit-progress-value
+					background $theme-color
+
+				&::-webkit-progress-bar
+					background rgba($theme-color, 0.1)
+
+			> .progress
+				display block
+				position absolute
+				bottom 0
+				right 0
+				margin 0
+				width calc(100% - 44px)
+				height 8px
+				border none
+				border-radius 4px
+				background linear-gradient(
+					45deg,
+					lighten($theme-color, 30%) 25%,
+					$theme-color               25%,
+					$theme-color               50%,
+					lighten($theme-color, 30%) 50%,
+					lighten($theme-color, 30%) 75%,
+					$theme-color               75%,
+					$theme-color
+				)
+				background-size 32px 32px
+				animation bg 1.5s linear infinite
+
+				&.initing
+					opacity 0.3
+
+				@keyframes bg
+					from {background-position: 0 0;}
+					to   {background-position: -64px 32px;}
+
+</style>

From d3a4efab15312d1b7b6a483854b990263ef0cb29 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 20:59:30 +0900
Subject: [PATCH 106/286] wip

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

diff --git a/src/web/app/common/views/components/uploader.vue b/src/web/app/common/views/components/uploader.vue
index 1239bec55..740d03ea5 100644
--- a/src/web/app/common/views/components/uploader.vue
+++ b/src/web/app/common/views/components/uploader.vue
@@ -6,7 +6,7 @@
 			<p class="name">%fa:spinner .pulse%{{ ctx.name }}</p>
 			<p class="status">
 				<span class="initing" v-if="ctx.progress == undefined">%i18n:common.tags.mk-uploader.waiting%<mk-ellipsis/></span>
-				<span class="kb" v-if="ctx.progress != undefined">{{ String(Math.floor(ctx.progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }<i>KB</i></span>
+				<span class="kb" v-if="ctx.progress != undefined">{{ String(Math.floor(ctx.progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
 				<span class="percentage" v-if="ctx.progress != undefined">{{ Math.floor((ctx.progress.value / ctx.progress.max) * 100) }}</span>
 			</p>
 			<progress v-if="ctx.progress != undefined && ctx.progress.value != ctx.progress.max" :value="ctx.progress.value" :max="ctx.progress.max"></progress>

From fda3ba4e4d8163baeb1e242a76bb101bfba83c01 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 21:36:30 +0900
Subject: [PATCH 107/286] wip

---
 src/web/app/common/-tags/poll-editor.tag      | 121 ----------------
 .../common/views/components/poll-editor.vue   | 130 ++++++++++++++++++
 2 files changed, 130 insertions(+), 121 deletions(-)
 delete mode 100644 src/web/app/common/-tags/poll-editor.tag
 create mode 100644 src/web/app/common/views/components/poll-editor.vue

diff --git a/src/web/app/common/-tags/poll-editor.tag b/src/web/app/common/-tags/poll-editor.tag
deleted file mode 100644
index 0de26f654..000000000
--- a/src/web/app/common/-tags/poll-editor.tag
+++ /dev/null
@@ -1,121 +0,0 @@
-<mk-poll-editor>
-	<p class="caution" v-if="choices.length < 2">
-		%fa:exclamation-triangle%%i18n:common.tags.mk-poll-editor.no-only-one-choice%
-	</p>
-	<ul ref="choices">
-		<li each={ choice, i in choices }>
-			<input value={ choice } oninput={ oninput.bind(null, i) } placeholder={ '%i18n:common.tags.mk-poll-editor.choice-n%'.replace('{}', i + 1) }>
-			<button @click="remove.bind(null, i)" title="%i18n:common.tags.mk-poll-editor.remove%">
-				%fa:times%
-			</button>
-		</li>
-	</ul>
-	<button class="add" v-if="choices.length < 10" @click="add">%i18n:common.tags.mk-poll-editor.add%</button>
-	<button class="destroy" @click="destroy" title="%i18n:common.tags.mk-poll-editor.destroy%">
-		%fa:times%
-	</button>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding 8px
-
-			> .caution
-				margin 0 0 8px 0
-				font-size 0.8em
-				color #f00
-
-				> [data-fa]
-					margin-right 4px
-
-			> ul
-				display block
-				margin 0
-				padding 0
-				list-style none
-
-				> li
-					display block
-					margin 8px 0
-					padding 0
-					width 100%
-
-					&:first-child
-						margin-top 0
-
-					&:last-child
-						margin-bottom 0
-
-					> input
-						padding 6px
-						border solid 1px rgba($theme-color, 0.1)
-						border-radius 4px
-
-						&:hover
-							border-color rgba($theme-color, 0.2)
-
-						&:focus
-							border-color rgba($theme-color, 0.5)
-
-					> button
-						padding 4px 8px
-						color rgba($theme-color, 0.4)
-
-						&:hover
-							color rgba($theme-color, 0.6)
-
-						&:active
-							color darken($theme-color, 30%)
-
-			> .add
-				margin 8px 0 0 0
-				vertical-align top
-				color $theme-color
-
-			> .destroy
-				position absolute
-				top 0
-				right 0
-				padding 4px 8px
-				color rgba($theme-color, 0.4)
-
-				&:hover
-					color rgba($theme-color, 0.6)
-
-				&:active
-					color darken($theme-color, 30%)
-
-	</style>
-	<script lang="typescript">
-		this.choices = ['', ''];
-
-		this.oninput = (i, e) => {
-			this.choices[i] = e.target.value;
-		};
-
-		this.add = () => {
-			this.choices.push('');
-			this.update();
-			this.$refs.choices.childNodes[this.choices.length - 1].childNodes[0].focus();
-		};
-
-		this.remove = (i) => {
-			this.choices = this.choices.filter((_, _i) => _i != i);
-			this.update();
-		};
-
-		this.destroy = () => {
-			this.opts.ondestroy();
-		};
-
-		this.get = () => {
-			return {
-				choices: this.choices.filter(choice => choice != '')
-			}
-		};
-
-		this.set = data => {
-			if (data.choices.length == 0) return;
-			this.choices = data.choices;
-		};
-	</script>
-</mk-poll-editor>
diff --git a/src/web/app/common/views/components/poll-editor.vue b/src/web/app/common/views/components/poll-editor.vue
new file mode 100644
index 000000000..2ae91bf25
--- /dev/null
+++ b/src/web/app/common/views/components/poll-editor.vue
@@ -0,0 +1,130 @@
+<template>
+<div class="mk-poll-editor">
+	<p class="caution" v-if="choices.length < 2">
+		%fa:exclamation-triangle%%i18n:common.tags.mk-poll-editor.no-only-one-choice%
+	</p>
+	<ul ref="choices">
+		<li v-for="(choice, i) in choices" :key="choice">
+			<input :value="choice" @input="onInput(i, $event)" :placeholder="'%i18n:common.tags.mk-poll-editor.choice-n%'.replace('{}', i + 1)">
+			<button @click="remove(i)" title="%i18n:common.tags.mk-poll-editor.remove%">
+				%fa:times%
+			</button>
+		</li>
+	</ul>
+	<button class="add" v-if="choices.length < 10" @click="add">%i18n:common.tags.mk-poll-editor.add%</button>
+	<button class="destroy" @click="destroy" title="%i18n:common.tags.mk-poll-editor.destroy%">
+		%fa:times%
+	</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			choices: ['', '']
+		};
+	},
+	methods: {
+		onInput(i, e) {
+			this.choices[i] = e.target.value; // TODO
+		},
+
+		add() {
+			this.choices.push('');
+			(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
+		},
+
+		remove(i) {
+			this.choices = this.choices.filter((_, _i) => _i != i);
+		},
+
+		destroy() {
+			this.$emit('destroyed');
+		},
+
+		get() {
+			return {
+				choices: this.choices.filter(choice => choice != '')
+			}
+		},
+
+		set(data) {
+			if (data.choices.length == 0) return;
+			this.choices = data.choices;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-poll-editor
+	padding 8px
+
+	> .caution
+		margin 0 0 8px 0
+		font-size 0.8em
+		color #f00
+
+		> [data-fa]
+			margin-right 4px
+
+	> ul
+		display block
+		margin 0
+		padding 0
+		list-style none
+
+		> li
+			display block
+			margin 8px 0
+			padding 0
+			width 100%
+
+			&:first-child
+				margin-top 0
+
+			&:last-child
+				margin-bottom 0
+
+			> input
+				padding 6px
+				border solid 1px rgba($theme-color, 0.1)
+				border-radius 4px
+
+				&:hover
+					border-color rgba($theme-color, 0.2)
+
+				&:focus
+					border-color rgba($theme-color, 0.5)
+
+			> button
+				padding 4px 8px
+				color rgba($theme-color, 0.4)
+
+				&:hover
+					color rgba($theme-color, 0.6)
+
+				&:active
+					color darken($theme-color, 30%)
+
+	> .add
+		margin 8px 0 0 0
+		vertical-align top
+		color $theme-color
+
+	> .destroy
+		position absolute
+		top 0
+		right 0
+		padding 4px 8px
+		color rgba($theme-color, 0.4)
+
+		&:hover
+			color rgba($theme-color, 0.6)
+
+		&:active
+			color darken($theme-color, 30%)
+
+</style>

From 7b733143516f8031848acc0c38597b027ec78196 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 21:51:17 +0900
Subject: [PATCH 108/286] wip

---
 src/web/app/common/-tags/special-message.tag  | 27 ------------
 .../views/components/special-message.vue      | 42 +++++++++++++++++++
 2 files changed, 42 insertions(+), 27 deletions(-)
 delete mode 100644 src/web/app/common/-tags/special-message.tag
 create mode 100644 src/web/app/common/views/components/special-message.vue

diff --git a/src/web/app/common/-tags/special-message.tag b/src/web/app/common/-tags/special-message.tag
deleted file mode 100644
index da903c632..000000000
--- a/src/web/app/common/-tags/special-message.tag
+++ /dev/null
@@ -1,27 +0,0 @@
-<mk-special-message>
-	<p v-if="m == 1 && d == 1">%i18n:common.tags.mk-special-message.new-year%</p>
-	<p v-if="m == 12 && d == 25">%i18n:common.tags.mk-special-message.christmas%</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			&:empty
-				display none
-
-			> p
-				margin 0
-				padding 4px
-				text-align center
-				font-size 14px
-				font-weight bold
-				text-transform uppercase
-				color #fff
-				background #ff1036
-
-	</style>
-	<script lang="typescript">
-		const now = new Date();
-		this.d = now.getDate();
-		this.m = now.getMonth() + 1;
-	</script>
-</mk-special-message>
diff --git a/src/web/app/common/views/components/special-message.vue b/src/web/app/common/views/components/special-message.vue
new file mode 100644
index 000000000..900afe178
--- /dev/null
+++ b/src/web/app/common/views/components/special-message.vue
@@ -0,0 +1,42 @@
+<template>
+<div class="mk-special-message">
+	<p v-if="m == 1 && d == 1">%i18n:common.tags.mk-special-message.new-year%</p>
+	<p v-if="m == 12 && d == 25">%i18n:common.tags.mk-special-message.christmas%</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			now: new Date()
+		};
+	},
+	computed: {
+		d(): number {
+			return now.getDate();
+		},
+		m(): number {
+			return now.getMonth() + 1;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-special-message
+	&:empty
+		display none
+
+	> p
+		margin 0
+		padding 4px
+		text-align center
+		font-size 14px
+		font-weight bold
+		text-transform uppercase
+		color #fff
+		background #ff1036
+
+</style>

From e0aba492ddfeb35a1905868f68abe526b3943b4e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 22:20:09 +0900
Subject: [PATCH 109/286] wip

---
 src/web/app/desktop/-tags/post-detail.tag     | 328 ------------------
 .../desktop/views/components/post-detail.vue  | 313 +++++++++++++++++
 2 files changed, 313 insertions(+), 328 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/post-detail.tag
 create mode 100644 src/web/app/desktop/views/components/post-detail.vue

diff --git a/src/web/app/desktop/-tags/post-detail.tag b/src/web/app/desktop/-tags/post-detail.tag
deleted file mode 100644
index 5f35ce6af..000000000
--- a/src/web/app/desktop/-tags/post-detail.tag
+++ /dev/null
@@ -1,328 +0,0 @@
-<mk-post-detail title={ title }>
-	<div class="main">
-		<button class="read-more" v-if="p.reply && p.reply.reply_id && context == null" title="会話をもっと読み込む" @click="loadContext" disabled={ contextFetching }>
-			<template v-if="!contextFetching">%fa:ellipsis-v%</template>
-			<template v-if="contextFetching">%fa:spinner .pulse%</template>
-		</button>
-		<div class="context">
-			<template each={ post in context }>
-				<mk-post-detail-sub post={ post }/>
-			</template>
-		</div>
-		<div class="reply-to" v-if="p.reply">
-			<mk-post-detail-sub post={ p.reply }/>
-		</div>
-		<div class="repost" v-if="isRepost">
-			<p>
-				<a class="avatar-anchor" href={ '/' + post.user.username } data-user-preview={ post.user_id }>
-					<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=32' } alt="avatar"/>
-				</a>
-				%fa:retweet%<a class="name" href={ '/' + post.user.username }>
-				{ post.user.name }
-			</a>
-			がRepost
-		</p>
-		</div>
-		<article>
-			<a class="avatar-anchor" href={ '/' + p.user.username }>
-				<img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=64' } alt="avatar" data-user-preview={ p.user.id }/>
-			</a>
-			<header>
-				<a class="name" href={ '/' + p.user.username } data-user-preview={ p.user.id }>{ p.user.name }</a>
-				<span class="username">@{ p.user.username }</span>
-				<a class="time" href={ '/' + p.user.username + '/' + p.id }>
-					<mk-time time={ p.created_at }/>
-				</a>
-			</header>
-			<div class="body">
-				<div class="text" ref="text"></div>
-				<div class="media" v-if="p.media">
-					<mk-images images={ p.media }/>
-				</div>
-				<mk-poll v-if="p.poll" post={ p }/>
-			</div>
-			<footer>
-				<mk-reactions-viewer post={ p }/>
-				<button @click="reply" title="返信">
-					%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
-				</button>
-				<button @click="repost" title="Repost">
-					%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
-				</button>
-				<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton" title="リアクション">
-					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
-				</button>
-				<button @click="menu" ref="menuButton">
-					%fa:ellipsis-h%
-				</button>
-			</footer>
-		</article>
-		<div class="replies" v-if="!compact">
-			<template each={ post in replies }>
-				<mk-post-detail-sub post={ post }/>
-			</template>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 0
-			overflow hidden
-			text-align left
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.1)
-			border-radius 8px
-
-			> .main
-
-				> .read-more
-					display block
-					margin 0
-					padding 10px 0
-					width 100%
-					font-size 1em
-					text-align center
-					color #999
-					cursor pointer
-					background #fafafa
-					outline none
-					border none
-					border-bottom solid 1px #eef0f2
-					border-radius 6px 6px 0 0
-
-					&:hover
-						background #f6f6f6
-
-					&:active
-						background #f0f0f0
-
-					&:disabled
-						color #ccc
-
-				> .context
-					> *
-						border-bottom 1px solid #eef0f2
-
-				> .repost
-					color #9dbb00
-					background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
-
-					> p
-						margin 0
-						padding 16px 32px
-
-						.avatar-anchor
-							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
-
-						[data-fa]
-							margin-right 4px
-
-						.name
-							font-weight bold
-
-					& + article
-						padding-top 8px
-
-				> .reply-to
-					border-bottom 1px solid #eef0f2
-
-				> article
-					padding 28px 32px 18px 32px
-
-					&:after
-						content ""
-						display block
-						clear both
-
-					&:hover
-						> .main > footer > button
-							color #888
-
-					> .avatar-anchor
-						display block
-						width 60px
-						height 60px
-
-						> .avatar
-							display block
-							width 60px
-							height 60px
-							margin 0
-							border-radius 8px
-							vertical-align bottom
-
-					> header
-						position absolute
-						top 28px
-						left 108px
-						width calc(100% - 108px)
-
-						> .name
-							display inline-block
-							margin 0
-							line-height 24px
-							color #777
-							font-size 18px
-							font-weight 700
-							text-align left
-							text-decoration none
-
-							&:hover
-								text-decoration underline
-
-						> .username
-							display block
-							text-align left
-							margin 0
-							color #ccc
-
-						> .time
-							position absolute
-							top 0
-							right 32px
-							font-size 1em
-							color #c0c0c0
-
-					> .body
-						padding 8px 0
-
-						> .text
-							cursor default
-							display block
-							margin 0
-							padding 0
-							overflow-wrap break-word
-							font-size 1.5em
-							color #717171
-
-							> mk-url-preview
-								margin-top 8px
-
-					> footer
-						font-size 1.2em
-
-						> button
-							margin 0 28px 0 0
-							padding 8px
-							background transparent
-							border none
-							font-size 1em
-							color #ddd
-							cursor pointer
-
-							&:hover
-								color #666
-
-							> .count
-								display inline
-								margin 0 0 0 8px
-								color #999
-
-							&.reacted
-								color $theme-color
-
-				> .replies
-					> *
-						border-top 1px solid #eef0f2
-
-	</style>
-	<script lang="typescript">
-		import compile from '../../common/scripts/text-compiler';
-		import dateStringify from '../../common/scripts/date-stringify';
-
-		this.mixin('api');
-		this.mixin('user-preview');
-
-		this.compact = this.opts.compact;
-		this.contextFetching = false;
-		this.context = null;
-		this.post = this.opts.post;
-		this.isRepost = this.post.repost != null;
-		this.p = this.isRepost ? this.post.repost : this.post;
-		this.p.reactions_count = this.p.reaction_counts ? Object.keys(this.p.reaction_counts).map(key => this.p.reaction_counts[key]).reduce((a, b) => a + b) : 0;
-		this.title = dateStringify(this.p.created_at);
-
-		this.on('mount', () => {
-			if (this.p.text) {
-				const tokens = this.p.ast;
-
-				this.$refs.text.innerHTML = compile(tokens);
-
-				Array.from(this.$refs.text.children).forEach(e => {
-					if (e.tagName == 'MK-URL') riot.mount(e);
-				});
-
-				// URLをプレビュー
-				tokens
-				.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
-				.map(t => {
-					riot.mount(this.$refs.text.appendChild(document.createElement('mk-url-preview')), {
-						url: t.url
-					});
-				});
-			}
-
-			// Get replies
-			if (!this.compact) {
-				this.api('posts/replies', {
-					post_id: this.p.id,
-					limit: 8
-				}).then(replies => {
-					this.update({
-						replies: replies
-					});
-				});
-			}
-		});
-
-		this.reply = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-post-form-window')), {
-				reply: this.p
-			});
-		};
-
-		this.repost = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-repost-form-window')), {
-				post: this.p
-			});
-		};
-
-		this.react = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), {
-				source: this.$refs.reactButton,
-				post: this.p
-			});
-		};
-
-		this.menu = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
-				source: this.$refs.menuButton,
-				post: this.p
-			});
-		};
-
-		this.loadContext = () => {
-			this.contextFetching = true;
-
-			// Fetch context
-			this.api('posts/context', {
-				post_id: this.p.reply_id
-			}).then(context => {
-				this.update({
-					contextFetching: false,
-					context: context.reverse()
-				});
-			});
-		};
-	</script>
-</mk-post-detail>
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
new file mode 100644
index 000000000..090a5bef6
--- /dev/null
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -0,0 +1,313 @@
+<template>
+<div class="mk-post-detail" :title="title">
+	<button class="read-more" v-if="p.reply && p.reply.reply_id && context == null" title="会話をもっと読み込む" @click="loadContext" disabled={ contextFetching }>
+		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
+		<template v-if="contextFetching">%fa:spinner .pulse%</template>
+	</button>
+	<div class="context">
+		<template each={ post in context }>
+			<mk-post-detail-sub post={ post }/>
+		</template>
+	</div>
+	<div class="reply-to" v-if="p.reply">
+		<mk-post-detail-sub post={ p.reply }/>
+	</div>
+	<div class="repost" v-if="isRepost">
+		<p>
+			<a class="avatar-anchor" href={ '/' + post.user.username } data-user-preview={ post.user_id }>
+				<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=32' } alt="avatar"/>
+			</a>
+			%fa:retweet%<a class="name" href={ '/' + post.user.username }>
+			{ post.user.name }
+		</a>
+		がRepost
+	</p>
+	</div>
+	<article>
+		<a class="avatar-anchor" href={ '/' + p.user.username }>
+			<img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=64' } alt="avatar" data-user-preview={ p.user.id }/>
+		</a>
+		<header>
+			<a class="name" href={ '/' + p.user.username } data-user-preview={ p.user.id }>{ p.user.name }</a>
+			<span class="username">@{ p.user.username }</span>
+			<a class="time" href={ '/' + p.user.username + '/' + p.id }>
+				<mk-time time={ p.created_at }/>
+			</a>
+		</header>
+		<div class="body">
+			<mk-post-html v-if="p.ast" :ast="p.ast" :i="$root.$data.os.i"/>
+			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+			<div class="media" v-if="p.media">
+				<mk-images images={ p.media }/>
+			</div>
+			<mk-poll v-if="p.poll" post={ p }/>
+		</div>
+		<footer>
+			<mk-reactions-viewer post={ p }/>
+			<button @click="reply" title="返信">
+				%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
+			</button>
+			<button @click="repost" title="Repost">
+				%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
+			</button>
+			<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton" title="リアクション">
+				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
+			</button>
+			<button @click="menu" ref="menuButton">
+				%fa:ellipsis-h%
+			</button>
+		</footer>
+	</article>
+	<div class="replies" v-if="!compact">
+		<template each={ post in replies }>
+			<mk-post-detail-sub post={ post }/>
+		</template>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../common/scripts/date-stringify';
+
+export default Vue.extend({
+	props: {
+		post: {
+			type: Object,
+			required: true
+		},
+		compact: {
+			default: false
+		}
+	},
+	data() {
+		return {
+			context: [],
+			contextFetching: false,
+			replies: [],
+		};
+	},
+	computed: {
+		isRepost(): boolean {
+			return this.post.repost != null;
+		},
+		p(): any {
+			return this.isRepost ? this.post.repost : this.post;
+		},
+		reactionsCount(): number {
+			return this.p.reaction_counts
+				? Object.keys(this.p.reaction_counts)
+					.map(key => this.p.reaction_counts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		title(): string {
+			return dateStringify(this.p.created_at);
+		},
+		urls(): string[] {
+			if (this.p.ast) {
+				return this.p.ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
+		}
+	},
+	mounted() {
+		// Get replies
+		if (!this.compact) {
+			this.$root.$data.os.api('posts/replies', {
+				post_id: this.p.id,
+				limit: 8
+			}).then(replies => {
+				this.replies = replies;
+			});
+		}
+	},
+	methods: {
+		fetchContext() {
+			this.contextFetching = true;
+
+			// Fetch context
+			this.$root.$data.os.api('posts/context', {
+				post_id: this.p.reply_id
+			}).then(context => {
+				this.contextFetching = false;
+				this.context = context.reverse();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-post-detail
+	margin 0
+	padding 0
+	overflow hidden
+	text-align left
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.1)
+	border-radius 8px
+
+	> .read-more
+		display block
+		margin 0
+		padding 10px 0
+		width 100%
+		font-size 1em
+		text-align center
+		color #999
+		cursor pointer
+		background #fafafa
+		outline none
+		border none
+		border-bottom solid 1px #eef0f2
+		border-radius 6px 6px 0 0
+
+		&:hover
+			background #f6f6f6
+
+		&:active
+			background #f0f0f0
+
+		&:disabled
+			color #ccc
+
+	> .context
+		> *
+			border-bottom 1px solid #eef0f2
+
+	> .repost
+		color #9dbb00
+		background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+
+		> p
+			margin 0
+			padding 16px 32px
+
+			.avatar-anchor
+				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
+
+			[data-fa]
+				margin-right 4px
+
+			.name
+				font-weight bold
+
+		& + article
+			padding-top 8px
+
+	> .reply-to
+		border-bottom 1px solid #eef0f2
+
+	> article
+		padding 28px 32px 18px 32px
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		&:hover
+			> .main > footer > button
+				color #888
+
+		> .avatar-anchor
+			display block
+			width 60px
+			height 60px
+
+			> .avatar
+				display block
+				width 60px
+				height 60px
+				margin 0
+				border-radius 8px
+				vertical-align bottom
+
+		> header
+			position absolute
+			top 28px
+			left 108px
+			width calc(100% - 108px)
+
+			> .name
+				display inline-block
+				margin 0
+				line-height 24px
+				color #777
+				font-size 18px
+				font-weight 700
+				text-align left
+				text-decoration none
+
+				&:hover
+					text-decoration underline
+
+			> .username
+				display block
+				text-align left
+				margin 0
+				color #ccc
+
+			> .time
+				position absolute
+				top 0
+				right 32px
+				font-size 1em
+				color #c0c0c0
+
+		> .body
+			padding 8px 0
+
+			> .text
+				cursor default
+				display block
+				margin 0
+				padding 0
+				overflow-wrap break-word
+				font-size 1.5em
+				color #717171
+
+				> mk-url-preview
+					margin-top 8px
+
+		> footer
+			font-size 1.2em
+
+			> button
+				margin 0 28px 0 0
+				padding 8px
+				background transparent
+				border none
+				font-size 1em
+				color #ddd
+				cursor pointer
+
+				&:hover
+					color #666
+
+				> .count
+					display inline
+					margin 0 0 0 8px
+					color #999
+
+				&.reacted
+					color $theme-color
+
+	> .replies
+		> *
+			border-top 1px solid #eef0f2
+
+</style>

From df136a7fb2599d049e59bdc6cf3d714d7ddbceff Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 14 Feb 2018 22:27:26 +0900
Subject: [PATCH 110/286] wip

---
 src/web/app/desktop/-tags/post-detail-sub.tag | 149 ------------------
 .../views/components/post-detail-sub.vue      | 125 +++++++++++++++
 2 files changed, 125 insertions(+), 149 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/post-detail-sub.tag
 create mode 100644 src/web/app/desktop/views/components/post-detail-sub.vue

diff --git a/src/web/app/desktop/-tags/post-detail-sub.tag b/src/web/app/desktop/-tags/post-detail-sub.tag
deleted file mode 100644
index 208805670..000000000
--- a/src/web/app/desktop/-tags/post-detail-sub.tag
+++ /dev/null
@@ -1,149 +0,0 @@
-<mk-post-detail-sub title={ title }>
-	<a class="avatar-anchor" href={ '/' + post.user.username }>
-		<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar" data-user-preview={ post.user_id }/>
-	</a>
-	<div class="main">
-		<header>
-			<div class="left">
-				<a class="name" href={ '/' + post.user.username } data-user-preview={ post.user_id }>{ post.user.name }</a>
-				<span class="username">@{ post.user.username }</span>
-			</div>
-			<div class="right">
-				<a class="time" href={ '/' + post.user.username + '/' + post.id }>
-					<mk-time time={ post.created_at }/>
-				</a>
-			</div>
-		</header>
-		<div class="body">
-			<div class="text" ref="text"></div>
-			<div class="media" v-if="post.media">
-				<mk-images images={ post.media }/>
-			</div>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 20px 32px
-			background #fdfdfd
-
-			&:after
-				content ""
-				display block
-				clear both
-
-			&:hover
-				> .main > footer > button
-					color #888
-
-			> .avatar-anchor
-				display block
-				float left
-				margin 0 16px 0 0
-
-				> .avatar
-					display block
-					width 44px
-					height 44px
-					margin 0
-					border-radius 4px
-					vertical-align bottom
-
-			> .main
-				float left
-				width calc(100% - 60px)
-
-				> header
-					margin-bottom 4px
-					white-space nowrap
-
-					&:after
-						content ""
-						display block
-						clear both
-
-					> .left
-						float left
-
-						> .name
-							display inline
-							margin 0
-							padding 0
-							color #777
-							font-size 1em
-							font-weight 700
-							text-align left
-							text-decoration none
-
-							&:hover
-								text-decoration underline
-
-						> .username
-							text-align left
-							margin 0 0 0 8px
-							color #ccc
-
-					> .right
-						float right
-
-						> .time
-							font-size 0.9em
-							color #c0c0c0
-
-				> .body
-
-					> .text
-						cursor default
-						display block
-						margin 0
-						padding 0
-						overflow-wrap break-word
-						font-size 1em
-						color #717171
-
-						> mk-url-preview
-							margin-top 8px
-
-	</style>
-	<script lang="typescript">
-		import compile from '../../common/scripts/text-compiler';
-		import dateStringify from '../../common/scripts/date-stringify';
-
-		this.mixin('api');
-		this.mixin('user-preview');
-
-		this.post = this.opts.post;
-		this.title = dateStringify(this.post.created_at);
-
-		this.on('mount', () => {
-			if (this.post.text) {
-				const tokens = this.post.ast;
-
-				this.$refs.text.innerHTML = compile(tokens);
-
-				Array.from(this.$refs.text.children).forEach(e => {
-					if (e.tagName == 'MK-URL') riot.mount(e);
-				});
-			}
-		});
-
-		this.like = () => {
-			if (this.post.is_liked) {
-				this.api('posts/likes/delete', {
-					post_id: this.post.id
-				}).then(() => {
-					this.post.is_liked = false;
-					this.update();
-				});
-			} else {
-				this.api('posts/likes/create', {
-					post_id: this.post.id
-				}).then(() => {
-					this.post.is_liked = true;
-					this.update();
-				});
-			}
-		};
-	</script>
-</mk-post-detail-sub>
diff --git a/src/web/app/desktop/views/components/post-detail-sub.vue b/src/web/app/desktop/views/components/post-detail-sub.vue
new file mode 100644
index 000000000..42f8be3b1
--- /dev/null
+++ b/src/web/app/desktop/views/components/post-detail-sub.vue
@@ -0,0 +1,125 @@
+<template>
+<div class="mk-post-detail-sub" :title="title">
+	<a class="avatar-anchor" href={ '/' + post.user.username }>
+		<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar" data-user-preview={ post.user_id }/>
+	</a>
+	<div class="main">
+		<header>
+			<div class="left">
+				<a class="name" href={ '/' + post.user.username } data-user-preview={ post.user_id }>{ post.user.name }</a>
+				<span class="username">@{ post.user.username }</span>
+			</div>
+			<div class="right">
+				<a class="time" href={ '/' + post.user.username + '/' + post.id }>
+					<mk-time time={ post.created_at }/>
+				</a>
+			</div>
+		</header>
+		<div class="body">
+			<mk-post-html v-if="post.ast" :ast="post.ast" :i="$root.$data.os.i"/>
+			<div class="media" v-if="post.media">
+				<mk-images images={ post.media }/>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../../common/scripts/date-stringify';
+
+export default Vue.extend({
+	props: ['post'],
+	computed: {
+		title(): string {
+			return dateStringify(this.post.created_at);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-post-detail-sub
+	margin 0
+	padding 20px 32px
+	background #fdfdfd
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	&:hover
+		> .main > footer > button
+			color #888
+
+	> .avatar-anchor
+		display block
+		float left
+		margin 0 16px 0 0
+
+		> .avatar
+			display block
+			width 44px
+			height 44px
+			margin 0
+			border-radius 4px
+			vertical-align bottom
+
+	> .main
+		float left
+		width calc(100% - 60px)
+
+		> header
+			margin-bottom 4px
+			white-space nowrap
+
+			&:after
+				content ""
+				display block
+				clear both
+
+			> .left
+				float left
+
+				> .name
+					display inline
+					margin 0
+					padding 0
+					color #777
+					font-size 1em
+					font-weight 700
+					text-align left
+					text-decoration none
+
+					&:hover
+						text-decoration underline
+
+				> .username
+					text-align left
+					margin 0 0 0 8px
+					color #ccc
+
+			> .right
+				float right
+
+				> .time
+					font-size 0.9em
+					color #c0c0c0
+
+		> .body
+
+			> .text
+				cursor default
+				display block
+				margin 0
+				padding 0
+				overflow-wrap break-word
+				font-size 1em
+				color #717171
+
+				> mk-url-preview
+					margin-top 8px
+
+</style>

From c70e56dd1d60b1c49fce6aef3824effc6582db76 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 00:32:13 +0900
Subject: [PATCH 111/286] wip

---
 src/web/app/desktop/-tags/pages/user.tag      |  27 -
 src/web/app/desktop/-tags/user.tag            | 852 ------------------
 .../pages/user/user-followers-you-know.vue    |  79 ++
 .../desktop/views/pages/user/user-friends.vue | 117 +++
 .../desktop/views/pages/user/user-header.vue  | 189 ++++
 .../desktop/views/pages/user/user-home.vue    |  90 ++
 .../desktop/views/pages/user/user-photos.vue  |  89 ++
 .../desktop/views/pages/user/user-profile.vue | 142 +++
 src/web/app/desktop/views/pages/user/user.vue |  43 +
 9 files changed, 749 insertions(+), 879 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/pages/user.tag
 delete mode 100644 src/web/app/desktop/-tags/user.tag
 create mode 100644 src/web/app/desktop/views/pages/user/user-followers-you-know.vue
 create mode 100644 src/web/app/desktop/views/pages/user/user-friends.vue
 create mode 100644 src/web/app/desktop/views/pages/user/user-header.vue
 create mode 100644 src/web/app/desktop/views/pages/user/user-home.vue
 create mode 100644 src/web/app/desktop/views/pages/user/user-photos.vue
 create mode 100644 src/web/app/desktop/views/pages/user/user-profile.vue
 create mode 100644 src/web/app/desktop/views/pages/user/user.vue

diff --git a/src/web/app/desktop/-tags/pages/user.tag b/src/web/app/desktop/-tags/pages/user.tag
deleted file mode 100644
index abed2ef02..000000000
--- a/src/web/app/desktop/-tags/pages/user.tag
+++ /dev/null
@@ -1,27 +0,0 @@
-<mk-user-page>
-	<mk-ui ref="ui">
-		<mk-user ref="user" user={ parent.user } page={ parent.opts.page }/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import Progress from '../../../common/scripts/loading';
-
-		this.user = this.opts.user;
-
-		this.on('mount', () => {
-			Progress.start();
-
-			this.$refs.ui.refs.user.on('user-fetched', user => {
-				Progress.set(0.5);
-				document.title = user.name + ' | Misskey';
-			});
-
-			this.$refs.ui.refs.user.on('loaded', () => {
-				Progress.done();
-			});
-		});
-	</script>
-</mk-user-page>
diff --git a/src/web/app/desktop/-tags/user.tag b/src/web/app/desktop/-tags/user.tag
deleted file mode 100644
index 8221926f4..000000000
--- a/src/web/app/desktop/-tags/user.tag
+++ /dev/null
@@ -1,852 +0,0 @@
-<mk-user>
-	<div class="user" v-if="!fetching">
-		<header>
-			<mk-user-header user={ user }/>
-		</header>
-		<mk-user-home v-if="page == 'home'" user={ user }/>
-		<mk-user-graphs v-if="page == 'graphs'" user={ user }/>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> .user
-				> header
-					> mk-user-header
-						overflow hidden
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.username = this.opts.user;
-		this.page = this.opts.page ? this.opts.page : 'home';
-		this.fetching = true;
-		this.user = null;
-
-		this.on('mount', () => {
-			this.api('users/show', {
-				username: this.username
-			}).then(user => {
-				this.update({
-					fetching: false,
-					user: user
-				});
-				this.$emit('loaded');
-			});
-		});
-	</script>
-</mk-user>
-
-<mk-user-header data-is-dark-background={ user.banner_url != null }>
-	<div class="banner-container" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=2048)' : '' }>
-		<div class="banner" ref="banner" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=2048)' : '' } @click="onUpdateBanner"></div>
-	</div>
-	<div class="fade"></div>
-	<div class="container">
-		<img class="avatar" src={ user.avatar_url + '?thumbnail&size=150' } alt="avatar"/>
-		<div class="title">
-			<p class="name" href={ '/' + user.username }>{ user.name }</p>
-			<p class="username">@{ user.username }</p>
-			<p class="location" v-if="user.profile.location">%fa:map-marker%{ user.profile.location }</p>
-		</div>
-		<footer>
-			<a href={ '/' + user.username } data-active={ parent.page == 'home' }>%fa:home%概要</a>
-			<a href={ '/' + user.username + '/media' } data-active={ parent.page == 'media' }>%fa:image%メディア</a>
-			<a href={ '/' + user.username + '/graphs' } data-active={ parent.page == 'graphs' }>%fa:chart-bar%グラフ</a>
-		</footer>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			$banner-height = 320px
-			$footer-height = 58px
-
-			display block
-			background #f7f7f7
-			box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
-
-			&[data-is-dark-background]
-				> .banner-container
-					> .banner
-						background-color #383838
-
-				> .fade
-					background linear-gradient(transparent, rgba(0, 0, 0, 0.7))
-
-				> .container
-					> .title
-						color #fff
-
-						> .name
-							text-shadow 0 0 8px #000
-
-			> .banner-container
-				height $banner-height
-				overflow hidden
-				background-size cover
-				background-position center
-
-				> .banner
-					height 100%
-					background-color #f5f5f5
-					background-size cover
-					background-position center
-
-			> .fade
-				$fade-hight = 78px
-
-				position absolute
-				top ($banner-height - $fade-hight)
-				left 0
-				width 100%
-				height $fade-hight
-
-			> .container
-				max-width 1200px
-				margin 0 auto
-
-				> .avatar
-					display block
-					position absolute
-					bottom 16px
-					left 16px
-					z-index 2
-					width 160px
-					height 160px
-					margin 0
-					border solid 3px #fff
-					border-radius 8px
-					box-shadow 1px 1px 3px rgba(0, 0, 0, 0.2)
-
-				> .title
-					position absolute
-					bottom $footer-height
-					left 0
-					width 100%
-					padding 0 0 8px 195px
-					color #656565
-					font-family '游ゴシック', 'YuGothic', 'ヒラギノ角ゴ ProN W3', 'Hiragino Kaku Gothic ProN', 'Meiryo', 'メイリオ', sans-serif
-
-					> .name
-						display block
-						margin 0
-						line-height 40px
-						font-weight bold
-						font-size 2em
-
-					> .username
-					> .location
-						display inline-block
-						margin 0 16px 0 0
-						line-height 20px
-						opacity 0.8
-
-						> i
-							margin-right 4px
-
-				> footer
-					z-index 1
-					height $footer-height
-					padding-left 195px
-
-					> a
-						display inline-block
-						margin 0
-						padding 0 16px
-						height $footer-height
-						line-height $footer-height
-						color #555
-
-						&[data-active]
-							border-bottom solid 4px $theme-color
-
-						> i
-							margin-right 6px
-
-					> button
-						display block
-						position absolute
-						top 0
-						right 0
-						margin 8px
-						padding 0
-						width $footer-height - 16px
-						line-height $footer-height - 16px - 2px
-						font-size 1.2em
-						color #777
-						border solid 1px #eee
-						border-radius 4px
-
-						&:hover
-							color #555
-							border solid 1px #ddd
-
-	</style>
-	<script lang="typescript">
-		import updateBanner from '../scripts/update-banner';
-
-		this.mixin('i');
-
-		this.user = this.opts.user;
-
-		this.on('mount', () => {
-			window.addEventListener('load', this.scroll);
-			window.addEventListener('scroll', this.scroll);
-			window.addEventListener('resize', this.scroll);
-		});
-
-		this.on('unmount', () => {
-			window.removeEventListener('load', this.scroll);
-			window.removeEventListener('scroll', this.scroll);
-			window.removeEventListener('resize', this.scroll);
-		});
-
-		this.scroll = () => {
-			const top = window.scrollY;
-
-			const z = 1.25; // 奥行き(小さいほど奥)
-			const pos = -(top / z);
-			this.$refs.banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
-
-			const blur = top / 32
-			if (blur <= 10) this.$refs.banner.style.filter = `blur(${blur}px)`;
-		};
-
-		this.onUpdateBanner = () => {
-			if (!this.SIGNIN || this.I.id != this.user.id) return;
-
-			updateBanner(this.I, i => {
-				this.user.banner_url = i.banner_url;
-				this.update();
-			});
-		};
-	</script>
-</mk-user-header>
-
-<mk-user-profile>
-	<div class="friend-form" v-if="SIGNIN && I.id != user.id">
-		<mk-big-follow-button user={ user }/>
-		<p class="followed" v-if="user.is_followed">%i18n:desktop.tags.mk-user.follows-you%</p>
-		<p v-if="user.is_muted">%i18n:desktop.tags.mk-user.muted% <a @click="unmute">%i18n:desktop.tags.mk-user.unmute%</a></p>
-		<p v-if="!user.is_muted"><a @click="mute">%i18n:desktop.tags.mk-user.mute%</a></p>
-	</div>
-	<div class="description" v-if="user.description">{ user.description }</div>
-	<div class="birthday" v-if="user.profile.birthday">
-		<p>%fa:birthday-cake%{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' } ({ age(user.profile.birthday) }歳)</p>
-	</div>
-	<div class="twitter" v-if="user.twitter">
-		<p>%fa:B twitter%<a href={ 'https://twitter.com/' + user.twitter.screen_name } target="_blank">@{ user.twitter.screen_name }</a></p>
-	</div>
-	<div class="status">
-	  <p class="posts-count">%fa:angle-right%<a>{ user.posts_count }</a><b>ポスト</b></p>
-		<p class="following">%fa:angle-right%<a @click="showFollowing">{ user.following_count }</a>人を<b>フォロー</b></p>
-		<p class="followers">%fa:angle-right%<a @click="showFollowers">{ user.followers_count }</a>人の<b>フォロワー</b></p>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> *:first-child
-				border-top none !important
-
-			> .friend-form
-				padding 16px
-				border-top solid 1px #eee
-
-				> mk-big-follow-button
-					width 100%
-
-				> .followed
-					margin 12px 0 0 0
-					padding 0
-					text-align center
-					line-height 24px
-					font-size 0.8em
-					color #71afc7
-					background #eefaff
-					border-radius 4px
-
-			> .description
-				padding 16px
-				color #555
-				border-top solid 1px #eee
-
-			> .birthday
-				padding 16px
-				color #555
-				border-top solid 1px #eee
-
-				> p
-					margin 0
-
-					> i
-						margin-right 8px
-
-			> .twitter
-				padding 16px
-				color #555
-				border-top solid 1px #eee
-
-				> p
-					margin 0
-
-					> i
-						margin-right 8px
-
-			> .status
-				padding 16px
-				color #555
-				border-top solid 1px #eee
-
-				> p
-					margin 8px 0
-
-					> i
-						margin-left 8px
-						margin-right 8px
-
-	</style>
-	<script lang="typescript">
-		this.age = require('s-age');
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.user = this.opts.user;
-
-		this.showFollowing = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-user-following-window')), {
-				user: this.user
-			});
-		};
-
-		this.showFollowers = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-user-followers-window')), {
-				user: this.user
-			});
-		};
-
-		this.mute = () => {
-			this.api('mute/create', {
-				user_id: this.user.id
-			}).then(() => {
-				this.user.is_muted = true;
-				this.update();
-			}, e => {
-				alert('error');
-			});
-		};
-
-		this.unmute = () => {
-			this.api('mute/delete', {
-				user_id: this.user.id
-			}).then(() => {
-				this.user.is_muted = false;
-				this.update();
-			}, e => {
-				alert('error');
-			});
-		};
-	</script>
-</mk-user-profile>
-
-<mk-user-photos>
-	<p class="title">%fa:camera%%i18n:desktop.tags.mk-user.photos.title%</p>
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.photos.loading%<mk-ellipsis/></p>
-	<div class="stream" v-if="!initializing && images.length > 0">
-		<template each={ image in images }>
-			<div class="img" style={ 'background-image: url(' + image.url + '?thumbnail&size=256)' }></div>
-		</template>
-	</div>
-	<p class="empty" v-if="!initializing && images.length == 0">%i18n:desktop.tags.mk-user.photos.no-photos%</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> .title
-				z-index 1
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> i
-					margin-right 4px
-
-			> .stream
-				display -webkit-flex
-				display -moz-flex
-				display -ms-flex
-				display flex
-				justify-content center
-				flex-wrap wrap
-				padding 8px
-
-				> .img
-					flex 1 1 33%
-					width 33%
-					height 80px
-					background-position center center
-					background-size cover
-					background-clip content-box
-					border solid 2px transparent
-
-			> .initializing
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> i
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		import isPromise from '../../common/scripts/is-promise';
-
-		this.mixin('api');
-
-		this.images = [];
-		this.initializing = true;
-		this.user = null;
-		this.userPromise = isPromise(this.opts.user)
-			? this.opts.user
-			: Promise.resolve(this.opts.user);
-
-		this.on('mount', () => {
-			this.userPromise.then(user => {
-				this.update({
-					user: user
-				});
-
-				this.api('users/posts', {
-					user_id: this.user.id,
-					with_media: true,
-					limit: 9
-				}).then(posts => {
-					this.initializing = false;
-					posts.forEach(post => {
-						post.media.forEach(media => {
-							if (this.images.length < 9) this.images.push(media);
-						});
-					});
-					this.update();
-				});
-			});
-		});
-	</script>
-</mk-user-photos>
-
-<mk-user-frequently-replied-users>
-	<p class="title">%fa:users%%i18n:desktop.tags.mk-user.frequently-replied-users.title%</p>
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p>
-	<div class="user" v-if="!initializing && users.length != 0" each={ _user in users }>
-		<a class="avatar-anchor" href={ '/' + _user.username }>
-			<img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ _user.id }/>
-		</a>
-		<div class="body">
-			<a class="name" href={ '/' + _user.username } data-user-preview={ _user.id }>{ _user.name }</a>
-			<p class="username">@{ _user.username }</p>
-		</div>
-		<mk-follow-button user={ _user }/>
-	</div>
-	<p class="empty" v-if="!initializing && users.length == 0">%i18n:desktop.tags.mk-user.frequently-replied-users.no-users%</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> .title
-				z-index 1
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> i
-					margin-right 4px
-
-			> .initializing
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> i
-					margin-right 4px
-
-			> .user
-				padding 16px
-				border-bottom solid 1px #eee
-
-				&:last-child
-					border-bottom none
-
-				&:after
-					content ""
-					display block
-					clear both
-
-				> .avatar-anchor
-					display block
-					float left
-					margin 0 12px 0 0
-
-					> .avatar
-						display block
-						width 42px
-						height 42px
-						margin 0
-						border-radius 8px
-						vertical-align bottom
-
-				> .body
-					float left
-					width calc(100% - 54px)
-
-					> .name
-						margin 0
-						font-size 16px
-						line-height 24px
-						color #555
-
-					> .username
-						display block
-						margin 0
-						font-size 15px
-						line-height 16px
-						color #ccc
-
-				> mk-follow-button
-					position absolute
-					top 16px
-					right 16px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.user = this.opts.user;
-		this.initializing = true;
-
-		this.on('mount', () => {
-			this.api('users/get_frequently_replied_users', {
-				user_id: this.user.id,
-				limit: 4
-			}).then(docs => {
-				this.update({
-					users: docs.map(doc => doc.user),
-					initializing: false
-				});
-			});
-		});
-	</script>
-</mk-user-frequently-replied-users>
-
-<mk-user-followers-you-know>
-	<p class="title">%fa:users%%i18n:desktop.tags.mk-user.followers-you-know.title%</p>
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p>
-	<div v-if="!initializing && users.length > 0">
-	<template each={ user in users }>
-		<a href={ '/' + user.username }><img src={ user.avatar_url + '?thumbnail&size=64' } alt={ user.name }/></a>
-	</template>
-	</div>
-	<p class="empty" v-if="!initializing && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> .title
-				z-index 1
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> i
-					margin-right 4px
-
-			> div
-				padding 8px
-
-				> a
-					display inline-block
-					margin 4px
-
-					> img
-						width 48px
-						height 48px
-						vertical-align bottom
-						border-radius 100%
-
-			> .initializing
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> i
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.user = this.opts.user;
-		this.initializing = true;
-
-		this.on('mount', () => {
-			this.api('users/followers', {
-				user_id: this.user.id,
-				iknow: true,
-				limit: 16
-			}).then(x => {
-				this.update({
-					users: x.users,
-					initializing: false
-				});
-			});
-		});
-	</script>
-</mk-user-followers-you-know>
-
-<mk-user-home>
-	<div>
-		<div ref="left">
-			<mk-user-profile user={ user }/>
-			<mk-user-photos user={ user }/>
-			<mk-user-followers-you-know v-if="SIGNIN && I.id !== user.id" user={ user }/>
-			<p>%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time time={ user.last_used_at }/></b></p>
-		</div>
-	</div>
-	<main>
-		<mk-post-detail v-if="user.pinned_post" post={ user.pinned_post } compact={ true }/>
-		<mk-user-timeline ref="tl" user={ user }/>
-	</main>
-	<div>
-		<div ref="right">
-			<mk-calendar-widget warp={ warp } start={ new Date(user.created_at) }/>
-			<mk-activity-widget user={ user }/>
-			<mk-user-frequently-replied-users user={ user }/>
-			<div class="nav"><mk-nav-links/></div>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display flex
-			justify-content center
-			margin 0 auto
-			max-width 1200px
-
-			> main
-			> div > div
-				> *:not(:last-child)
-					margin-bottom 16px
-
-			> main
-				padding 16px
-				width calc(100% - 275px * 2)
-
-				> mk-user-timeline
-					border solid 1px rgba(0, 0, 0, 0.075)
-					border-radius 6px
-
-			> div
-				width 275px
-				margin 0
-
-				&:first-child > div
-					padding 16px 0 16px 16px
-
-					> p
-						display block
-						margin 0
-						padding 0 12px
-						text-align center
-						font-size 0.8em
-						color #aaa
-
-				&:last-child > div
-					padding 16px 16px 16px 0
-
-					> .nav
-						padding 16px
-						font-size 12px
-						color #aaa
-						background #fff
-						border solid 1px rgba(0, 0, 0, 0.075)
-						border-radius 6px
-
-						a
-							color #999
-
-						i
-							color #ccc
-
-	</style>
-	<script lang="typescript">
-		import ScrollFollower from '../scripts/scroll-follower';
-
-		this.mixin('i');
-
-		this.user = this.opts.user;
-
-		this.on('mount', () => {
-			this.$refs.tl.on('loaded', () => {
-				this.$emit('loaded');
-			});
-
-			this.scrollFollowerLeft = new ScrollFollower(this.$refs.left, this.parent.root.getBoundingClientRect().top);
-			this.scrollFollowerRight = new ScrollFollower(this.$refs.right, this.parent.root.getBoundingClientRect().top);
-		});
-
-		this.on('unmount', () => {
-			this.scrollFollowerLeft.dispose();
-			this.scrollFollowerRight.dispose();
-		});
-
-		this.warp = date => {
-			this.$refs.tl.warp(date);
-		};
-	</script>
-</mk-user-home>
-
-<mk-user-graphs>
-	<section>
-		<div>
-			<h1>%fa:pencil-alt%投稿</h1>
-			<mk-user-graphs-activity-chart user={ opts.user }/>
-		</div>
-	</section>
-	<section>
-		<div>
-			<h1>フォロー/フォロワー</h1>
-			<mk-user-friends-graph user={ opts.user }/>
-		</div>
-	</section>
-	<section>
-		<div>
-			<h1>いいね</h1>
-			<mk-user-likes-graph user={ opts.user }/>
-		</div>
-	</section>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> section
-				margin 16px 0
-				color #666
-				border-bottom solid 1px rgba(0, 0, 0, 0.1)
-
-				> div
-					max-width 1200px
-					margin 0 auto
-					padding 0 16px
-
-					> h1
-						margin 0 0 16px 0
-						padding 0
-						font-size 1.3em
-
-						> i
-							margin-right 8px
-
-	</style>
-	<script lang="typescript">
-		this.on('mount', () => {
-			this.$emit('loaded');
-		});
-	</script>
-</mk-user-graphs>
-
-<mk-user-graphs-activity-chart>
-	<svg v-if="data" ref="canvas" viewBox="0 0 365 1" preserveAspectRatio="none">
-		<g each={ d, i in data.reverse() }>
-			<rect width="0.8" riot-height={ d.postsH }
-				riot-x={ i + 0.1 } riot-y={ 1 - d.postsH - d.repliesH - d.repostsH }
-				fill="#41ddde"/>
-			<rect width="0.8" riot-height={ d.repliesH }
-				riot-x={ i + 0.1 } riot-y={ 1 - d.repliesH - d.repostsH }
-				fill="#f7796c"/>
-			<rect width="0.8" riot-height={ d.repostsH }
-				riot-x={ i + 0.1 } riot-y={ 1 - d.repostsH }
-				fill="#a1de41"/>
-			</g>
-	</svg>
-	<p>直近1年間分の統計です。一番右が現在で、一番左が1年前です。青は通常の投稿、赤は返信、緑はRepostをそれぞれ表しています。</p>
-	<p>
-		<span>だいたい*1日に<b>{ averageOfAllTypePostsEachDays }回</b>投稿(返信、Repost含む)しています。</span><br>
-		<span>だいたい*1日に<b>{ averageOfPostsEachDays }回</b>投稿(通常の)しています。</span><br>
-		<span>だいたい*1日に<b>{ averageOfRepliesEachDays }回</b>返信しています。</span><br>
-		<span>だいたい*1日に<b>{ averageOfRepostsEachDays }回</b>Repostしています。</span><br>
-	</p>
-	<p>* 中央値</p>
-
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> svg
-				display block
-				width 100%
-				height 180px
-
-				> rect
-					transform-origin center
-
-	</style>
-	<script lang="typescript">
-		import getMedian from '../../common/scripts/get-median';
-
-		this.mixin('api');
-
-		this.user = this.opts.user;
-
-		this.on('mount', () => {
-			this.api('aggregation/users/activity', {
-				user_id: this.user.id,
-				limit: 365
-			}).then(data => {
-				data.forEach(d => d.total = d.posts + d.replies + d.reposts);
-				this.peak = Math.max.apply(null, data.map(d => d.total));
-				data.forEach(d => {
-					d.postsH = d.posts / this.peak;
-					d.repliesH = d.replies / this.peak;
-					d.repostsH = d.reposts / this.peak;
-				});
-
-				this.update({
-					data,
-					averageOfAllTypePostsEachDays: getMedian(data.map(d => d.total)),
-					averageOfPostsEachDays: getMedian(data.map(d => d.posts)),
-					averageOfRepliesEachDays: getMedian(data.map(d => d.replies)),
-					averageOfRepostsEachDays: getMedian(data.map(d => d.reposts))
-				});
-			});
-		});
-	</script>
-</mk-user-graphs-activity-chart>
diff --git a/src/web/app/desktop/views/pages/user/user-followers-you-know.vue b/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
new file mode 100644
index 000000000..419008175
--- /dev/null
+++ b/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
@@ -0,0 +1,79 @@
+<template>
+<div class="mk-user-followers-you-know">
+	<p class="title">%fa:users%%i18n:desktop.tags.mk-user.followers-you-know.title%</p>
+	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p>
+	<div v-if="!initializing && users.length > 0">
+	<template each={ user in users }>
+		<a href={ '/' + user.username }><img src={ user.avatar_url + '?thumbnail&size=64' } alt={ user.name }/></a>
+	</template>
+	</div>
+	<p class="empty" v-if="!initializing && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	data() {
+		return {
+			users: [],
+			fetching: true
+		};
+	},
+	mounted() {
+		this.$root.$data.os.api('users/followers', {
+			user_id: this.user.id,
+			iknow: true,
+			limit: 16
+		}).then(x => {
+			this.fetching = false;
+			this.users = x.users;
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-followers-you-know
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	> .title
+		z-index 1
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> i
+			margin-right 4px
+
+	> div
+		padding 8px
+
+		> a
+			display inline-block
+			margin 4px
+
+			> img
+				width 48px
+				height 48px
+				vertical-align bottom
+				border-radius 100%
+
+	> .initializing
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> i
+			margin-right 4px
+
+</style>
diff --git a/src/web/app/desktop/views/pages/user/user-friends.vue b/src/web/app/desktop/views/pages/user/user-friends.vue
new file mode 100644
index 000000000..eed874897
--- /dev/null
+++ b/src/web/app/desktop/views/pages/user/user-friends.vue
@@ -0,0 +1,117 @@
+<template>
+<div class="mk-user-friends">
+	<p class="title">%fa:users%%i18n:desktop.tags.mk-user.frequently-replied-users.title%</p>
+	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p>
+	<div class="user" v-if="!fetching && users.length != 0" each={ _user in users }>
+		<a class="avatar-anchor" href={ '/' + _user.username }>
+			<img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ _user.id }/>
+		</a>
+		<div class="body">
+			<a class="name" href={ '/' + _user.username } data-user-preview={ _user.id }>{ _user.name }</a>
+			<p class="username">@{ _user.username }</p>
+		</div>
+		<mk-follow-button user={ _user }/>
+	</div>
+	<p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.frequently-replied-users.no-users%</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	data() {
+		return {
+			users: [],
+			fetching: true
+		};
+	},
+	mounted() {
+		this.$root.$data.os.api('users/get_frequently_replied_users', {
+			user_id: this.user.id,
+			limit: 4
+		}).then(docs => {
+			this.fetching = false;
+			this.users = docs.map(doc => doc.user);
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-friends
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	> .title
+		z-index 1
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> i
+			margin-right 4px
+
+	> .initializing
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> i
+			margin-right 4px
+
+	> .user
+		padding 16px
+		border-bottom solid 1px #eee
+
+		&:last-child
+			border-bottom none
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		> .avatar-anchor
+			display block
+			float left
+			margin 0 12px 0 0
+
+			> .avatar
+				display block
+				width 42px
+				height 42px
+				margin 0
+				border-radius 8px
+				vertical-align bottom
+
+		> .body
+			float left
+			width calc(100% - 54px)
+
+			> .name
+				margin 0
+				font-size 16px
+				line-height 24px
+				color #555
+
+			> .username
+				display block
+				margin 0
+				font-size 15px
+				line-height 16px
+				color #ccc
+
+		> mk-follow-button
+			position absolute
+			top 16px
+			right 16px
+
+</style>
diff --git a/src/web/app/desktop/views/pages/user/user-header.vue b/src/web/app/desktop/views/pages/user/user-header.vue
new file mode 100644
index 000000000..07f206d24
--- /dev/null
+++ b/src/web/app/desktop/views/pages/user/user-header.vue
@@ -0,0 +1,189 @@
+<template>
+<div class="mk-user-header" :data-is-dark-background="user.banner_url != null">
+	<div class="banner-container" :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=2048)` : ''">
+		<div class="banner" ref="banner" :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=2048)` : ''" @click="onBannerClick"></div>
+	</div>
+	<div class="fade"></div>
+	<div class="container">
+		<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=150`" alt="avatar"/>
+		<div class="title">
+			<p class="name">{{ user.name }}</p>
+			<p class="username">@{{ user.username }}</p>
+			<p class="location" v-if="user.profile.location">%fa:map-marker%{{ user.profile.location }}</p>
+		</div>
+		<footer>
+			<a :href="`/${user.username}`" :data-active="$parent.page == 'home'">%fa:home%概要</a>
+			<a :href="`/${user.username}/media`" :data-active="$parent.page == 'media'">%fa:image%メディア</a>
+			<a :href="`/${user.username}/graphs`" :data-active="$parent.page == 'graphs'">%fa:chart-bar%グラフ</a>
+		</footer>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import updateBanner from '../../../scripts/update-banner';
+
+export default Vue.extend({
+	props: ['user'],
+	mounted() {
+		window.addEventListener('load', this.onScroll);
+		window.addEventListener('scroll', this.onScroll);
+		window.addEventListener('resize', this.onScroll);
+	},
+	beforeDestroy() {
+		window.removeEventListener('load', this.onScroll);
+		window.removeEventListener('scroll', this.onScroll);
+		window.removeEventListener('resize', this.onScroll);
+	},
+	methods: {
+		onScroll() {
+			const banner = this.$refs.banner as any;
+
+			const top = window.scrollY;
+
+			const z = 1.25; // 奥行き(小さいほど奥)
+			const pos = -(top / z);
+			banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
+
+			const blur = top / 32
+			if (blur <= 10) banner.style.filter = `blur(${blur}px)`;
+		},
+
+		onBannerClick() {
+			if (!this.$root.$data.os.isSignedIn || this.$root.$data.os.i.id != this.user.id) return;
+
+			updateBanner(this.$root.$data.os.i, i => {
+				this.user.banner_url = i.banner_url;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-header
+	$banner-height = 320px
+	$footer-height = 58px
+
+	overflow hidden
+	background #f7f7f7
+	box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
+
+	&[data-is-dark-background]
+		> .banner-container
+			> .banner
+				background-color #383838
+
+		> .fade
+			background linear-gradient(transparent, rgba(0, 0, 0, 0.7))
+
+		> .container
+			> .title
+				color #fff
+
+				> .name
+					text-shadow 0 0 8px #000
+
+	> .banner-container
+		height $banner-height
+		overflow hidden
+		background-size cover
+		background-position center
+
+		> .banner
+			height 100%
+			background-color #f5f5f5
+			background-size cover
+			background-position center
+
+	> .fade
+		$fade-hight = 78px
+
+		position absolute
+		top ($banner-height - $fade-hight)
+		left 0
+		width 100%
+		height $fade-hight
+
+	> .container
+		max-width 1200px
+		margin 0 auto
+
+		> .avatar
+			display block
+			position absolute
+			bottom 16px
+			left 16px
+			z-index 2
+			width 160px
+			height 160px
+			margin 0
+			border solid 3px #fff
+			border-radius 8px
+			box-shadow 1px 1px 3px rgba(0, 0, 0, 0.2)
+
+		> .title
+			position absolute
+			bottom $footer-height
+			left 0
+			width 100%
+			padding 0 0 8px 195px
+			color #656565
+			font-family '游ゴシック', 'YuGothic', 'ヒラギノ角ゴ ProN W3', 'Hiragino Kaku Gothic ProN', 'Meiryo', 'メイリオ', sans-serif
+
+			> .name
+				display block
+				margin 0
+				line-height 40px
+				font-weight bold
+				font-size 2em
+
+			> .username
+			> .location
+				display inline-block
+				margin 0 16px 0 0
+				line-height 20px
+				opacity 0.8
+
+				> i
+					margin-right 4px
+
+		> footer
+			z-index 1
+			height $footer-height
+			padding-left 195px
+
+			> a
+				display inline-block
+				margin 0
+				padding 0 16px
+				height $footer-height
+				line-height $footer-height
+				color #555
+
+				&[data-active]
+					border-bottom solid 4px $theme-color
+
+				> i
+					margin-right 6px
+
+			> button
+				display block
+				position absolute
+				top 0
+				right 0
+				margin 8px
+				padding 0
+				width $footer-height - 16px
+				line-height $footer-height - 16px - 2px
+				font-size 1.2em
+				color #777
+				border solid 1px #eee
+				border-radius 4px
+
+				&:hover
+					color #555
+					border solid 1px #ddd
+
+</style>
diff --git a/src/web/app/desktop/views/pages/user/user-home.vue b/src/web/app/desktop/views/pages/user/user-home.vue
new file mode 100644
index 000000000..926a1f571
--- /dev/null
+++ b/src/web/app/desktop/views/pages/user/user-home.vue
@@ -0,0 +1,90 @@
+<template>
+<div class="mk-user-home">
+	<div>
+		<div ref="left">
+			<mk-user-profile :user="user"/>
+			<mk-user-photos :user="user"/>
+			<mk-user-followers-you-know v-if="$root.$data.os.isSignedIn && $root.$data.os.i.id != user.id" :user="user"/>
+			<p>%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.last_used_at"/></b></p>
+		</div>
+	</div>
+	<main>
+		<mk-post-detail v-if="user.pinned_post" :post="user.pinned_post" compact/>
+		<mk-user-timeline ref="tl" :user="user"/>
+	</main>
+	<div>
+		<div ref="right">
+			<mk-calendar-widget @warp="warp" :start="new Date(user.created_at)"/>
+			<mk-activity-widget :user="user"/>
+			<mk-user-friends :user="user"/>
+			<div class="nav"><mk-nav-links/></div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	methods: {
+		warp(date) {
+			(this.$refs.tl as any).warp(date);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-home
+	display flex
+	justify-content center
+	margin 0 auto
+	max-width 1200px
+
+	> main
+	> div > div
+		> *:not(:last-child)
+			margin-bottom 16px
+
+	> main
+		padding 16px
+		width calc(100% - 275px * 2)
+
+		> mk-user-timeline
+			border solid 1px rgba(0, 0, 0, 0.075)
+			border-radius 6px
+
+	> div
+		width 275px
+		margin 0
+
+		&:first-child > div
+			padding 16px 0 16px 16px
+
+			> p
+				display block
+				margin 0
+				padding 0 12px
+				text-align center
+				font-size 0.8em
+				color #aaa
+
+		&:last-child > div
+			padding 16px 16px 16px 0
+
+			> .nav
+				padding 16px
+				font-size 12px
+				color #aaa
+				background #fff
+				border solid 1px rgba(0, 0, 0, 0.075)
+				border-radius 6px
+
+				a
+					color #999
+
+				i
+					color #ccc
+
+</style>
diff --git a/src/web/app/desktop/views/pages/user/user-photos.vue b/src/web/app/desktop/views/pages/user/user-photos.vue
new file mode 100644
index 000000000..fc51b9789
--- /dev/null
+++ b/src/web/app/desktop/views/pages/user/user-photos.vue
@@ -0,0 +1,89 @@
+<template>
+<div class="mk-user-photos">
+	<p class="title">%fa:camera%%i18n:desktop.tags.mk-user.photos.title%</p>
+	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.photos.loading%<mk-ellipsis/></p>
+	<div class="stream" v-if="!fetching && images.length > 0">
+		<div v-for="image in images" :key="image.id"
+			class="img"
+			:style="`background-image: url(${image.url}?thumbnail&size=256)`"
+		></div>
+	</div>
+	<p class="empty" v-if="!fetching && images.length == 0">%i18n:desktop.tags.mk-user.photos.no-photos%</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	data() {
+		return {
+			images: [],
+			fetching: true
+		};
+	},
+	mounted() {
+		this.$root.$data.os.api('users/posts', {
+			user_id: this.user.id,
+			with_media: true,
+			limit: 9
+		}).then(posts => {
+			this.fetching = false;
+			posts.forEach(post => {
+				post.media.forEach(media => {
+					if (this.images.length < 9) this.images.push(media);
+				});
+			});
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-photos
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	> .title
+		z-index 1
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> i
+			margin-right 4px
+
+	> .stream
+		display -webkit-flex
+		display -moz-flex
+		display -ms-flex
+		display flex
+		justify-content center
+		flex-wrap wrap
+		padding 8px
+
+		> .img
+			flex 1 1 33%
+			width 33%
+			height 80px
+			background-position center center
+			background-size cover
+			background-clip content-box
+			border solid 2px transparent
+
+	> .initializing
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> i
+			margin-right 4px
+
+</style>
diff --git a/src/web/app/desktop/views/pages/user/user-profile.vue b/src/web/app/desktop/views/pages/user/user-profile.vue
new file mode 100644
index 000000000..6b88b47ac
--- /dev/null
+++ b/src/web/app/desktop/views/pages/user/user-profile.vue
@@ -0,0 +1,142 @@
+<template>
+<div class="mk-user-profile">
+	<div class="friend-form" v-if="$root.$data.os.isSignedIn && $root.$data.os.i.id != user.id">
+		<mk-follow-button :user="user" size="big"/>
+		<p class="followed" v-if="user.is_followed">%i18n:desktop.tags.mk-user.follows-you%</p>
+		<p v-if="user.is_muted">%i18n:desktop.tags.mk-user.muted% <a @click="unmute">%i18n:desktop.tags.mk-user.unmute%</a></p>
+		<p v-if="!user.is_muted"><a @click="mute">%i18n:desktop.tags.mk-user.mute%</a></p>
+	</div>
+	<div class="description" v-if="user.description">{{ user.description }}</div>
+	<div class="birthday" v-if="user.profile.birthday">
+		<p>%fa:birthday-cake%{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</p>
+	</div>
+	<div class="twitter" v-if="user.twitter">
+		<p>%fa:B twitter%<a :href="`https://twitter.com/${user.twitter.screen_name}`" target="_blank">@{{ user.twitter.screen_name }}</a></p>
+	</div>
+	<div class="status">
+	  <p class="posts-count">%fa:angle-right%<a>{{ user.posts_count }}</a><b>投稿</b></p>
+		<p class="following">%fa:angle-right%<a @click="showFollowing">{{ user.following_count }}</a>人を<b>フォロー</b></p>
+		<p class="followers">%fa:angle-right%<a @click="showFollowers">{{ user.followers_count }}</a>人の<b>フォロワー</b></p>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+const age = require('s-age');
+
+export default Vue.extend({
+	props: ['user'],
+	computed: {
+		age(): number {
+			return age(this.user.profile.birthday);
+		}
+	},
+	methods: {
+		showFollowing() {
+			document.body.appendChild(new MkUserFollowingWindow({
+				parent: this,
+				propsData: {
+					user: this.user
+				}
+			}).$mount().$el);
+		},
+
+		showFollowers() {
+			document.body.appendChild(new MkUserFollowersWindow({
+				parent: this,
+				propsData: {
+					user: this.user
+				}
+			}).$mount().$el);
+		},
+
+		mute() {
+			this.$root.$data.os.api('mute/create', {
+				user_id: this.user.id
+			}).then(() => {
+				this.user.is_muted = true;
+			}, e => {
+				alert('error');
+			});
+		},
+
+		unmute() {
+			this.$root.$data.os.api('mute/delete', {
+				user_id: this.user.id
+			}).then(() => {
+				this.user.is_muted = false;
+			}, e => {
+				alert('error');
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-profile
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	> *:first-child
+		border-top none !important
+
+	> .friend-form
+		padding 16px
+		border-top solid 1px #eee
+
+		> mk-big-follow-button
+			width 100%
+
+		> .followed
+			margin 12px 0 0 0
+			padding 0
+			text-align center
+			line-height 24px
+			font-size 0.8em
+			color #71afc7
+			background #eefaff
+			border-radius 4px
+
+	> .description
+		padding 16px
+		color #555
+		border-top solid 1px #eee
+
+	> .birthday
+		padding 16px
+		color #555
+		border-top solid 1px #eee
+
+		> p
+			margin 0
+
+			> i
+				margin-right 8px
+
+	> .twitter
+		padding 16px
+		color #555
+		border-top solid 1px #eee
+
+		> p
+			margin 0
+
+			> i
+				margin-right 8px
+
+	> .status
+		padding 16px
+		color #555
+		border-top solid 1px #eee
+
+		> p
+			margin 8px 0
+
+			> i
+				margin-left 8px
+				margin-right 8px
+
+</style>
diff --git a/src/web/app/desktop/views/pages/user/user.vue b/src/web/app/desktop/views/pages/user/user.vue
new file mode 100644
index 000000000..109ee6037
--- /dev/null
+++ b/src/web/app/desktop/views/pages/user/user.vue
@@ -0,0 +1,43 @@
+<template>
+<mk-ui>
+	<div class="user" v-if="!fetching">
+		<mk-user-header :user="user"/>
+		<mk-user-home v-if="page == 'home'" :user="user"/>
+		<mk-user-graphs v-if="page == 'graphs'" :user="user"/>
+	</div>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+	props: {
+		username: {
+			type: String
+		},
+		page: {
+			default: 'home'
+		}
+	},
+	data() {
+		return {
+			fetching: true,
+			user: null
+		};
+	},
+	mounted() {
+		Progress.start();
+		this.$root.$data.os.api('users/show', {
+			username: this.username
+		}).then(user => {
+			this.fetching = false;
+			this.user = user;
+			Progress.done();
+			document.title = user.name + ' | Misskey';
+		});
+	}
+});
+</script>
+

From a89aea142b75cd6eafd969752a197ca07f99f04d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 00:33:07 +0900
Subject: [PATCH 112/286] wip

---
 .../desktop/-tags/detailed-post-window.tag    | 80 -------------------
 1 file changed, 80 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/detailed-post-window.tag

diff --git a/src/web/app/desktop/-tags/detailed-post-window.tag b/src/web/app/desktop/-tags/detailed-post-window.tag
deleted file mode 100644
index 6803aeacf..000000000
--- a/src/web/app/desktop/-tags/detailed-post-window.tag
+++ /dev/null
@@ -1,80 +0,0 @@
-<mk-detailed-post-window>
-	<div class="bg" ref="bg" @click="bgClick"></div>
-	<div class="main" ref="main" v-if="!fetching">
-		<mk-post-detail ref="detail" post={ post }/>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			opacity 0
-
-			> .bg
-				display block
-				position fixed
-				z-index 1000
-				top 0
-				left 0
-				width 100%
-				height 100%
-				background rgba(0, 0, 0, 0.7)
-
-			> .main
-				display block
-				position fixed
-				z-index 1000
-				top 20%
-				left 0
-				right 0
-				margin 0 auto 0 auto
-				padding 0
-				width 638px
-				text-align center
-
-				> mk-post-detail
-					margin 0 auto
-
-	</style>
-	<script lang="typescript">
-		import * as anime from 'animejs';
-
-		this.mixin('api');
-
-		this.fetching = true;
-		this.post = null;
-
-		this.on('mount', () => {
-			anime({
-				targets: this.root,
-				opacity: 1,
-				duration: 100,
-				easing: 'linear'
-			});
-
-			this.api('posts/show', {
-				post_id: this.opts.post
-			}).then(post => {
-
-				this.update({
-					fetching: false,
-					post: post
-				});
-			});
-		});
-
-		this.close = () => {
-			this.$refs.bg.style.pointerEvents = 'none';
-			this.$refs.main.style.pointerEvents = 'none';
-			anime({
-				targets: this.root,
-				opacity: 0,
-				duration: 300,
-				easing: 'linear',
-				complete: () => this.$destroy()
-			});
-		};
-
-		this.bgClick = () => {
-			this.close();
-		};
-	</script>
-</mk-detailed-post-window>

From 1febbbb12c8bfb55dda785b8335b32bfb2111e37 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 01:07:09 +0900
Subject: [PATCH 113/286] wip

---
 src/web/app/common/define-widget.ts           | 51 +++++++++++++++++++
 .../views/components/widgets/profile.vue      |  5 ++
 webpack/module/rules/license.ts               |  2 +-
 3 files changed, 57 insertions(+), 1 deletion(-)
 create mode 100644 src/web/app/common/define-widget.ts
 create mode 100644 src/web/app/common/views/components/widgets/profile.vue

diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
new file mode 100644
index 000000000..9aed5a890
--- /dev/null
+++ b/src/web/app/common/define-widget.ts
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+
+export default function(data: {
+	name: string;
+	props: any;
+}) {
+	return Vue.extend({
+		props: {
+			wid: {
+				type: String,
+				required: true
+			},
+			place: {
+				type: String,
+				required: true
+			},
+			wprops: {
+				type: Object,
+				required: false
+			}
+		},
+		computed: {
+			id(): string {
+				return this.wid;
+			}
+		},
+		data() {
+			return {
+				props: data.props
+			};
+		},
+		watch: {
+			props(newProps, oldProps) {
+				if (JSON.stringify(newProps) == JSON.stringify(oldProps)) return;
+				this.$root.$data.os.api('i/update_home', {
+					id: this.id,
+					data: newProps
+				}).then(() => {
+					this.$root.$data.os.i.client_settings.home.find(w => w.id == this.id).data = newProps;
+				});
+			}
+		},
+		created() {
+			if (this.props) {
+				Object.keys(this.wprops).forEach(prop => {
+					this.props[prop] = this.props.data.hasOwnProperty(prop) ? this.props.data[prop] : this.props[prop];
+				});
+			}
+		}
+	});
+}
diff --git a/src/web/app/common/views/components/widgets/profile.vue b/src/web/app/common/views/components/widgets/profile.vue
new file mode 100644
index 000000000..4a22d2391
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/profile.vue
@@ -0,0 +1,5 @@
+<template>
+<div class="mkw-profile">
+
+</div>
+</template>
diff --git a/webpack/module/rules/license.ts b/webpack/module/rules/license.ts
index de8b7d79f..e3aaefa2b 100644
--- a/webpack/module/rules/license.ts
+++ b/webpack/module/rules/license.ts
@@ -7,7 +7,7 @@ import { licenseHtml } from '../../../src/common/build/license';
 
 export default () => ({
 	enforce: 'pre',
-	test: /\.(tag|js)$/,
+	test: /\.(vue|js)$/,
 	exclude: /node_modules/,
 	loader: StringReplacePlugin.replace({
 		replacements: [{

From ad459f6dd3b8dacd09d30fb559cb1b4553da5f5f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 01:41:31 +0900
Subject: [PATCH 114/286] wip

---
 src/web/app/common/define-widget.ts           |  12 +-
 .../views/components/widgets/profile.vue      | 124 +++++++++++++++++-
 .../desktop/-tags/home-widgets/profile.tag    | 116 ----------------
 3 files changed, 129 insertions(+), 123 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/profile.tag

diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index 9aed5a890..5102ee1ab 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -1,8 +1,8 @@
 import Vue from 'vue';
 
-export default function(data: {
+export default function<T extends object>(data: {
 	name: string;
-	props: any;
+	props: T;
 }) {
 	return Vue.extend({
 		props: {
@@ -10,7 +10,7 @@ export default function(data: {
 				type: String,
 				required: true
 			},
-			place: {
+			wplace: {
 				type: String,
 				required: true
 			},
@@ -42,8 +42,10 @@ export default function(data: {
 		},
 		created() {
 			if (this.props) {
-				Object.keys(this.wprops).forEach(prop => {
-					this.props[prop] = this.props.data.hasOwnProperty(prop) ? this.props.data[prop] : this.props[prop];
+				Object.keys(this.props).forEach(prop => {
+					if (this.wprops.hasOwnProperty(prop)) {
+						this.props[prop] = this.wprops[prop];
+					}
 				});
 			}
 		}
diff --git a/src/web/app/common/views/components/widgets/profile.vue b/src/web/app/common/views/components/widgets/profile.vue
index 4a22d2391..1fb756333 100644
--- a/src/web/app/common/views/components/widgets/profile.vue
+++ b/src/web/app/common/views/components/widgets/profile.vue
@@ -1,5 +1,125 @@
 <template>
-<div class="mkw-profile">
-
+<div class="mkw-profile"
+	data-compact={ data.design == 1 || data.design == 2 }
+	data-melt={ data.design == 2 }
+>
+	<div class="banner"
+		style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=256)' : '' }
+		title="クリックでバナー編集"
+		@click="wapi_setBanner"
+	></div>
+	<img class="avatar"
+		src={ I.avatar_url + '?thumbnail&size=96' }
+		@click="wapi_setAvatar"
+		alt="avatar"
+		title="クリックでアバター編集"
+		:v-user-preview={ I.id }
+	/>
+	<a class="name" href={ '/' + I.username }>{ I.name }</a>
+	<p class="username">@{ I.username }</p>
 </div>
 </template>
+
+<script lang="ts">
+import define from '../../../define-widget';
+export default define({
+	name: 'profile',
+	props: {
+		design: 0
+	}
+}).extend({
+	methods: {
+		func() {
+			if (this.props.design == 3) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-profile
+	overflow hidden
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	&[data-compact]
+		> .banner:before
+			content ""
+			display block
+			width 100%
+			height 100%
+			background rgba(0, 0, 0, 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)
+
+		> .name
+			position absolute
+			top 0
+			left 92px
+			margin 0
+			line-height 100px
+			color #fff
+			text-shadow 0 0 8px rgba(0, 0, 0, 0.5)
+
+		> .username
+			display none
+
+	&[data-melt]
+		background transparent !important
+		border none !important
+
+		> .banner
+			visibility hidden
+
+		> .avatar
+			box-shadow none
+
+		> .name
+			color #666
+			text-shadow none
+
+	> .banner
+		height 100px
+		background-color #f5f5f5
+		background-size cover
+		background-position center
+		cursor pointer
+
+	> .avatar
+		display block
+		position absolute
+		top 76px
+		left 16px
+		width 58px
+		height 58px
+		margin 0
+		border solid 3px #fff
+		border-radius 8px
+		vertical-align bottom
+		cursor pointer
+
+	> .name
+		display block
+		margin 10px 0 0 84px
+		line-height 16px
+		font-weight bold
+		color #555
+
+	> .username
+		display block
+		margin 4px 0 8px 84px
+		line-height 16px
+		font-size 0.9em
+		color #999
+
+</style>
diff --git a/src/web/app/desktop/-tags/home-widgets/profile.tag b/src/web/app/desktop/-tags/home-widgets/profile.tag
deleted file mode 100644
index 02a1f0d5a..000000000
--- a/src/web/app/desktop/-tags/home-widgets/profile.tag
+++ /dev/null
@@ -1,116 +0,0 @@
-<mk-profile-home-widget data-compact={ data.design == 1 || data.design == 2 } data-melt={ data.design == 2 }>
-	<div class="banner" style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=256)' : '' } title="クリックでバナー編集" @click="setBanner"></div>
-	<img class="avatar" src={ I.avatar_url + '?thumbnail&size=96' } @click="setAvatar" alt="avatar" title="クリックでアバター編集" data-user-preview={ I.id }/>
-	<a class="name" href={ '/' + I.username }>{ I.name }</a>
-	<p class="username">@{ I.username }</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow hidden
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			&[data-compact]
-				> .banner:before
-					content ""
-					display block
-					width 100%
-					height 100%
-					background rgba(0, 0, 0, 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)
-
-				> .name
-					position absolute
-					top 0
-					left 92px
-					margin 0
-					line-height 100px
-					color #fff
-					text-shadow 0 0 8px rgba(0, 0, 0, 0.5)
-
-				> .username
-					display none
-
-			&[data-melt]
-				background transparent !important
-				border none !important
-
-				> .banner
-					visibility hidden
-
-				> .avatar
-					box-shadow none
-
-				> .name
-					color #666
-					text-shadow none
-
-			> .banner
-				height 100px
-				background-color #f5f5f5
-				background-size cover
-				background-position center
-				cursor pointer
-
-			> .avatar
-				display block
-				position absolute
-				top 76px
-				left 16px
-				width 58px
-				height 58px
-				margin 0
-				border solid 3px #fff
-				border-radius 8px
-				vertical-align bottom
-				cursor pointer
-
-			> .name
-				display block
-				margin 10px 0 0 84px
-				line-height 16px
-				font-weight bold
-				color #555
-
-			> .username
-				display block
-				margin 4px 0 8px 84px
-				line-height 16px
-				font-size 0.9em
-				color #999
-
-	</style>
-	<script lang="typescript">
-		import inputDialog from '../../scripts/input-dialog';
-		import updateAvatar from '../../scripts/update-avatar';
-		import updateBanner from '../../scripts/update-banner';
-
-		this.data = {
-			design: 0
-		};
-
-		this.mixin('widget');
-
-		this.mixin('user-preview');
-
-		this.setAvatar = () => {
-			updateAvatar(this.I);
-		};
-
-		this.setBanner = () => {
-			updateBanner(this.I);
-		};
-
-		this.func = () => {
-			if (++this.data.design == 3) this.data.design = 0;
-			this.save();
-		};
-	</script>
-</mk-profile-home-widget>

From b47fd29535a9d60b9d960d27e319a06f2f93d34f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 01:42:11 +0900
Subject: [PATCH 115/286] wip

---
 src/web/app/common/views/components/widgets/profile.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/common/views/components/widgets/profile.vue b/src/web/app/common/views/components/widgets/profile.vue
index 1fb756333..e589eb20b 100644
--- a/src/web/app/common/views/components/widgets/profile.vue
+++ b/src/web/app/common/views/components/widgets/profile.vue
@@ -30,7 +30,7 @@ export default define({
 }).extend({
 	methods: {
 		func() {
-			if (this.props.design == 3) {
+			if (this.props.design == 2) {
 				this.props.design = 0;
 			} else {
 				this.props.design++;

From ca604692628dcba95681964e8deec5ca75049c4e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 12:36:42 +0900
Subject: [PATCH 116/286] wip

---
 src/web/app/common/define-widget.ts           |   4 +-
 .../views/components/widgets/calendar.vue     | 192 ++++++++++++++++++
 .../views/components/widgets/donation.vue     |  45 ++++
 .../views/components/widgets/messaging.vue    |  59 ++++++
 .../common/views/components/widgets/nav.vue   |  29 +++
 .../views/components/widgets/photo-stream.vue | 122 +++++++++++
 .../views/components/widgets/profile.vue      |   4 +-
 .../views/components/widgets/slideshow.vue    | 154 ++++++++++++++
 .../common/views/components/widgets/tips.vue  | 109 ++++++++++
 .../desktop/-tags/home-widgets/calendar.tag   | 167 ---------------
 .../desktop/-tags/home-widgets/donation.tag   |  36 ----
 .../desktop/-tags/home-widgets/messaging.tag  |  52 -----
 .../app/desktop/-tags/home-widgets/nav.tag    |  23 ---
 .../-tags/home-widgets/photo-stream.tag       | 118 -----------
 .../desktop/-tags/home-widgets/slideshow.tag  | 151 --------------
 .../app/desktop/-tags/home-widgets/tips.tag   |  94 ---------
 webpack/plugins/index.ts                      |   4 +-
 17 files changed, 716 insertions(+), 647 deletions(-)
 create mode 100644 src/web/app/common/views/components/widgets/calendar.vue
 create mode 100644 src/web/app/common/views/components/widgets/donation.vue
 create mode 100644 src/web/app/common/views/components/widgets/messaging.vue
 create mode 100644 src/web/app/common/views/components/widgets/nav.vue
 create mode 100644 src/web/app/common/views/components/widgets/photo-stream.vue
 create mode 100644 src/web/app/common/views/components/widgets/slideshow.vue
 create mode 100644 src/web/app/common/views/components/widgets/tips.vue
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/calendar.tag
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/donation.tag
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/messaging.tag
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/nav.tag
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/photo-stream.tag
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/slideshow.tag
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/tips.tag

diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index 5102ee1ab..782a69a62 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -2,7 +2,7 @@ import Vue from 'vue';
 
 export default function<T extends object>(data: {
 	name: string;
-	props: T;
+	props?: T;
 }) {
 	return Vue.extend({
 		props: {
@@ -26,7 +26,7 @@ export default function<T extends object>(data: {
 		},
 		data() {
 			return {
-				props: data.props
+				props: data.props || {}
 			};
 		},
 		watch: {
diff --git a/src/web/app/common/views/components/widgets/calendar.vue b/src/web/app/common/views/components/widgets/calendar.vue
new file mode 100644
index 000000000..308f43cd9
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/calendar.vue
@@ -0,0 +1,192 @@
+<template>
+<div class="mkw-calendar"
+	:data-melt="props.design == 1"
+	:data-special="special"
+>
+	<div class="calendar" :data-is-holiday="isHoliday">
+		<p class="month-and-year">
+			<span class="year">{{ year }}年</span>
+			<span class="month">{{ month }}月</span>
+		</p>
+		<p class="day">{{ day }}日</p>
+		<p class="week-day">{{ weekDay }}曜日</p>
+	</div>
+	<div class="info">
+		<div>
+			<p>今日:<b>{{ dayP.toFixed(1) }}%</b></p>
+			<div class="meter">
+				<div class="val" :style="{ width: `${dayP}%` }"></div>
+			</div>
+		</div>
+		<div>
+			<p>今月:<b>{{ monthP.toFixed(1) }}%</b></p>
+			<div class="meter">
+				<div class="val" :style="{ width: `${monthP}%` }"></div>
+			</div>
+		</div>
+		<div>
+			<p>今年:<b>{{ yearP.toFixed(1) }}%</b></p>
+			<div class="meter">
+				<div class="val" :style="{ width: `${yearP}%` }"></div>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../define-widget';
+export default define({
+	name: 'calendar',
+	props: {
+		design: 0
+	}
+}).extend({
+	data() {
+		return {
+			now: new Date(),
+			year: null,
+			month: null,
+			day: null,
+			weekDay: null,
+			yearP: null,
+			dayP: null,
+			monthP: null,
+			isHoliday: null,
+			special: null,
+			clock: null
+		};
+	},
+	created() {
+		this.tick();
+		this.clock = setInterval(this.tick, 1000);
+	},
+	beforeDestroy() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		func() {
+			if (this.props.design == 2) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		},
+		tick() {
+			const now = new Date();
+			const nd = now.getDate();
+			const nm = now.getMonth();
+			const ny = now.getFullYear();
+
+			this.year = ny;
+			this.month = nm + 1;
+			this.day = nd;
+			this.weekDay = ['日', '月', '火', '水', '木', '金', '土'][now.getDay()];
+
+			const dayNumer   = now.getTime() - new Date(ny, nm, nd).getTime();
+			const dayDenom   = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/;
+			const monthNumer = now.getTime() - new Date(ny, nm, 1).getTime();
+			const monthDenom = new Date(ny, nm + 1, 1).getTime() - new Date(ny, nm, 1).getTime();
+			const yearNumer  = now.getTime() - new Date(ny, 0, 1).getTime();
+			const yearDenom  = new Date(ny + 1, 0, 1).getTime() - new Date(ny, 0, 1).getTime();
+
+			this.dayP   = dayNumer   / dayDenom   * 100;
+			this.monthP = monthNumer / monthDenom * 100;
+			this.yearP  = yearNumer  / yearDenom  * 100;
+
+			this.isHoliday = now.getDay() == 0 || now.getDay() == 6;
+
+			this.special =
+				nm == 0 && nd == 1 ? 'on-new-years-day' :
+				false;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-calendar
+	padding 16px 0
+	color #777
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	&[data-special='on-new-years-day']
+		border-color #ef95a0
+
+	&[data-melt]
+		background transparent
+		border none
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	> .calendar
+		float left
+		width 60%
+		text-align center
+
+		&[data-is-holiday]
+			> .day
+				color #ef95a0
+
+		> p
+			margin 0
+			line-height 18px
+			font-size 14px
+
+			> span
+				margin 0 4px
+
+		> .day
+			margin 10px 0
+			line-height 32px
+			font-size 28px
+
+	> .info
+		display block
+		float left
+		width 40%
+		padding 0 16px 0 0
+
+		> div
+			margin-bottom 8px
+
+			&:last-child
+				margin-bottom 4px
+
+			> p
+				margin 0 0 2px 0
+				font-size 12px
+				line-height 18px
+				color #888
+
+				> b
+					margin-left 2px
+
+			> .meter
+				width 100%
+				overflow hidden
+				background #eee
+				border-radius 8px
+
+				> .val
+					height 4px
+					background $theme-color
+
+			&:nth-child(1)
+				> .meter > .val
+					background #f7796c
+
+			&:nth-child(2)
+				> .meter > .val
+					background #a1de41
+
+			&:nth-child(3)
+				> .meter > .val
+					background #41ddde
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/donation.vue b/src/web/app/common/views/components/widgets/donation.vue
new file mode 100644
index 000000000..50adc531b
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/donation.vue
@@ -0,0 +1,45 @@
+<template>
+<div class="mkw-donation">
+	<article>
+		<h1>%fa:heart%%i18n:desktop.tags.mk-donation-home-widget.title%</h1>
+		<p>
+			{{ '%i18n:desktop.tags.mk-donation-home-widget.text%'.substr(0, '%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('{')) }}
+			<a href="/syuilo" data-user-preview="@syuilo">@syuilo</a>
+			{{ '%i18n:desktop.tags.mk-donation-home-widget.text%'.substr('%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('}') + 1) }}
+		</p>
+	</article>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../define-widget';
+export default define({
+	name: 'donation'
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-donation
+	background #fff
+	border solid 1px #ead8bb
+	border-radius 6px
+
+	> article
+		padding 20px
+
+		> h1
+			margin 0 0 5px 0
+			font-size 1em
+			color #888
+
+			> [data-fa]
+				margin-right 0.25em
+
+		> p
+			display block
+			z-index 1
+			margin 0
+			font-size 0.8em
+			color #999
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/messaging.vue b/src/web/app/common/views/components/widgets/messaging.vue
new file mode 100644
index 000000000..19ef70431
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/messaging.vue
@@ -0,0 +1,59 @@
+<template>
+<div class="mkw-messaging">
+	<p class="title" v-if="props.design == 0">%fa:comments%%i18n:desktop.tags.mk-messaging-home-widget.title%</p>
+	<mk-messaging ref="index" compact @navigate="navigate"/>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../define-widget';
+export default define({
+	name: 'messaging',
+	props: {
+		design: 0
+	}
+}).extend({
+	methods: {
+		navigate(user) {
+			if (this.platform == 'desktop') {
+				this.wapi_openMessagingRoomWindow(user);
+			} else {
+				// TODO: open room page in new tab
+			}
+		},
+		func() {
+			if (this.props.design == 1) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-messaging
+	overflow hidden
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	> .title
+		z-index 2
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> [data-fa]
+			margin-right 4px
+
+	> mk-messaging
+		max-height 250px
+		overflow auto
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/nav.vue b/src/web/app/common/views/components/widgets/nav.vue
new file mode 100644
index 000000000..77e1eea49
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/nav.vue
@@ -0,0 +1,29 @@
+<template>
+<div class="mkw-nav">
+	<mk-nav-links/>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../define-widget';
+export default define({
+	name: 'nav'
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-nav
+	padding 16px
+	font-size 12px
+	color #aaa
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	a
+		color #999
+
+	i
+		color #ccc
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/photo-stream.vue b/src/web/app/common/views/components/widgets/photo-stream.vue
new file mode 100644
index 000000000..12e568ca0
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/photo-stream.vue
@@ -0,0 +1,122 @@
+<template>
+<div class="mkw-photo-stream" :data-melt="props.design == 2">
+	<p class="title" v-if="props.design == 0">%fa:camera%%i18n:desktop.tags.mk-photo-stream-home-widget.title%</p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<div class="stream" v-if="!fetching && images.length > 0">
+		<div v-for="image in images" :key="image.id" class="img" :style="`background-image: url(${image.url}?thumbnail&size=256)`"></div>
+	</div>
+	<p class="empty" v-if="!fetching && images.length == 0">%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../define-widget';
+export default define({
+	name: 'photo-stream',
+	props: {
+		design: 0
+	}
+}).extend({
+	data() {
+		return {
+			images: [],
+			fetching: true,
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		this.connection = this.$root.$data.os.stream.getConnection();
+		this.connectionId = this.$root.$data.os.stream.use();
+
+		this.connection.on('drive_file_created', this.onDriveFileCreated);
+
+		this.$root.$data.os.api('drive/stream', {
+			type: 'image/*',
+			limit: 9
+		}).then(images => {
+			this.fetching = false;
+			this.images = images;
+		});
+	},
+	beforeDestroy() {
+		this.connection.off('drive_file_created', this.onDriveFileCreated);
+		this.$root.$data.os.stream.dispose(this.connectionId);
+	},
+	methods: {
+		onStreamDriveFileCreated(file) {
+			if (/^image\/.+$/.test(file.type)) {
+				this.images.unshift(file);
+				if (this.images.length > 9) this.images.pop();
+			}
+		},
+		func() {
+			if (this.props.design == 2) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-photo-stream
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	&[data-melt]
+		background transparent !important
+		border none !important
+
+		> .stream
+			padding 0
+
+			> .img
+				border solid 4px transparent
+				border-radius 8px
+
+	> .title
+		z-index 1
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> [data-fa]
+			margin-right 4px
+
+	> .stream
+		display -webkit-flex
+		display -moz-flex
+		display -ms-flex
+		display flex
+		justify-content center
+		flex-wrap wrap
+		padding 8px
+
+		> .img
+			flex 1 1 33%
+			width 33%
+			height 80px
+			background-position center center
+			background-size cover
+			border solid 2px transparent
+			border-radius 4px
+
+	> .fetching
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/profile.vue b/src/web/app/common/views/components/widgets/profile.vue
index e589eb20b..70902c7cf 100644
--- a/src/web/app/common/views/components/widgets/profile.vue
+++ b/src/web/app/common/views/components/widgets/profile.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mkw-profile"
-	data-compact={ data.design == 1 || data.design == 2 }
-	data-melt={ data.design == 2 }
+	:data-compact="props.design == 1 || props.design == 2"
+	:data-melt="props.design == 2"
 >
 	<div class="banner"
 		style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=256)' : '' }
diff --git a/src/web/app/common/views/components/widgets/slideshow.vue b/src/web/app/common/views/components/widgets/slideshow.vue
new file mode 100644
index 000000000..6dcd453e2
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/slideshow.vue
@@ -0,0 +1,154 @@
+<template>
+<div class="mkw-slideshow">
+	<div @click="choose">
+		<p v-if="data.folder === undefined">クリックしてフォルダを指定してください</p>
+		<p v-if="data.folder !== undefined && images.length == 0 && !fetching">このフォルダには画像がありません</p>
+		<div ref="slideA" class="slide a"></div>
+		<div ref="slideB" class="slide b"></div>
+	</div>
+	<button @click="resize">%fa:expand%</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+import define from '../../../define-widget';
+export default define({
+	name: 'slideshow',
+	props: {
+		folder: undefined,
+		size: 0
+	}
+}).extend({
+	data() {
+		return {
+			images: [],
+			fetching: true,
+			clock: null
+		};
+	},
+	mounted() {
+		Vue.nextTick(() => {
+			this.applySize();
+		});
+
+		if (this.props.folder !== undefined) {
+			this.fetch();
+		}
+
+		this.clock = setInterval(this.change, 10000);
+	},
+	beforeDestroy() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		applySize() {
+			let h;
+
+			if (this.props.size == 1) {
+				h = 250;
+			} else {
+				h = 170;
+			}
+
+			this.$el.style.height = `${h}px`;
+		},
+		resize() {
+			if (this.props.size == 1) {
+				this.props.size = 0;
+			} else {
+				this.props.size++;
+			}
+
+			this.applySize();
+		},
+		change() {
+			if (this.images.length == 0) return;
+
+			const index = Math.floor(Math.random() * this.images.length);
+			const img = `url(${ this.images[index].url }?thumbnail&size=1024)`;
+
+			(this.$refs.slideB as any).style.backgroundImage = img;
+
+			anime({
+				targets: this.$refs.slideB,
+				opacity: 1,
+				duration: 1000,
+				easing: 'linear',
+				complete: () => {
+					(this.$refs.slideA as any).style.backgroundImage = img;
+					anime({
+						targets: this.$refs.slideB,
+						opacity: 0,
+						duration: 0
+					});
+				}
+			});
+		},
+		fetch() {
+			this.fetching = true;
+
+			this.$root.$data.os.api('drive/files', {
+				folder_id: this.props.folder,
+				type: 'image/*',
+				limit: 100
+			}).then(images => {
+				this.fetching = false;
+				this.images = images;
+				(this.$refs.slideA as any).style.backgroundImage = '';
+				(this.$refs.slideB as any).style.backgroundImage = '';
+				this.change();
+			});
+		},
+		choose() {
+			this.wapi_selectDriveFolder().then(folder => {
+				this.props.folder = folder ? folder.id : null;
+				this.fetch();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-slideshow
+	overflow hidden
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	&:hover > button
+		display block
+
+	> button
+		position absolute
+		left 0
+		bottom 0
+		display none
+		padding 4px
+		font-size 24px
+		color #fff
+		text-shadow 0 0 8px #000
+
+	> div
+		width 100%
+		height 100%
+		cursor pointer
+
+		> *
+			pointer-events none
+
+		> .slide
+			position absolute
+			top 0
+			left 0
+			width 100%
+			height 100%
+			background-size cover
+			background-position center
+
+			&.b
+				opacity 0
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/tips.vue b/src/web/app/common/views/components/widgets/tips.vue
new file mode 100644
index 000000000..f38ecfe44
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/tips.vue
@@ -0,0 +1,109 @@
+<template>
+<div class="mkw-tips">
+	<p ref="tip">%fa:R lightbulb%<span v-html="tip"></span></p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+import define from '../../../define-widget';
+
+const tips = [
+	'<kbd>t</kbd>でタイムラインにフォーカスできます',
+	'<kbd>p</kbd>または<kbd>n</kbd>で投稿フォームを開きます',
+	'投稿フォームにはファイルをドラッグ&ドロップできます',
+	'投稿フォームにクリップボードにある画像データをペーストできます',
+	'ドライブにファイルをドラッグ&ドロップしてアップロードできます',
+	'ドライブでファイルをドラッグしてフォルダ移動できます',
+	'ドライブでフォルダをドラッグしてフォルダ移動できます',
+	'ホームは設定からカスタマイズできます',
+	'MisskeyはMIT Licenseです',
+	'タイムマシンウィジェットを利用すると、簡単に過去のタイムラインに遡れます',
+	'投稿の ... をクリックして、投稿をユーザーページにピン留めできます',
+	'ドライブの容量は(デフォルトで)1GBです',
+	'投稿に添付したファイルは全てドライブに保存されます',
+	'ホームのカスタマイズ中、ウィジェットを右クリックしてデザインを変更できます',
+	'タイムライン上部にもウィジェットを設置できます',
+	'投稿をダブルクリックすると詳細が見れます',
+	'「**」でテキストを囲むと**強調表示**されます',
+	'チャンネルウィジェットを利用すると、よく利用するチャンネルを素早く確認できます',
+	'いくつかのウィンドウはブラウザの外に切り離すことができます',
+	'カレンダーウィジェットのパーセンテージは、経過の割合を示しています',
+	'APIを利用してbotの開発なども行えます',
+	'MisskeyはLINEを通じてでも利用できます',
+	'まゆかわいいよまゆ',
+	'Misskeyは2014年にサービスを開始しました',
+	'対応ブラウザではMisskeyを開いていなくても通知を受け取れます'
+]
+
+export default define({
+	name: 'tips'
+}).extend({
+	data() {
+		return {
+			tip: null,
+			clock: null
+		};
+	},
+	mounted() {
+		Vue.nextTick(() => {
+			this.set();
+		});
+
+		this.clock = setInterval(this.change, 20000);
+	},
+	beforeDestroy() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		set() {
+			this.tip = tips[Math.floor(Math.random() * tips.length)];
+		},
+		change() {
+			anime({
+				targets: this.$refs.tip,
+				opacity: 0,
+				duration: 500,
+				easing: 'linear',
+				complete: this.set
+			});
+
+			setTimeout(() => {
+				anime({
+					targets: this.$refs.tip,
+					opacity: 1,
+					duration: 500,
+					easing: 'linear'
+				});
+			}, 500);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-tips
+	overflow visible !important
+
+	> p
+		display block
+		margin 0
+		padding 0 12px
+		text-align center
+		font-size 0.7em
+		color #999
+
+		> [data-fa]
+			margin-right 4px
+
+		kbd
+			display inline
+			padding 0 6px
+			margin 0 2px
+			font-size 1em
+			font-family inherit
+			border solid 1px #999
+			border-radius 2px
+
+</style>
diff --git a/src/web/app/desktop/-tags/home-widgets/calendar.tag b/src/web/app/desktop/-tags/home-widgets/calendar.tag
deleted file mode 100644
index 46d47662b..000000000
--- a/src/web/app/desktop/-tags/home-widgets/calendar.tag
+++ /dev/null
@@ -1,167 +0,0 @@
-<mk-calendar-home-widget data-melt={ data.design == 1 } data-special={ special }>
-	<div class="calendar" data-is-holiday={ isHoliday }>
-		<p class="month-and-year"><span class="year">{ year }年</span><span class="month">{ month }月</span></p>
-		<p class="day">{ day }日</p>
-		<p class="week-day">{ weekDay }曜日</p>
-	</div>
-	<div class="info">
-		<div>
-			<p>今日:<b>{ dayP.toFixed(1) }%</b></p>
-			<div class="meter">
-				<div class="val" style={ 'width:' + dayP + '%' }></div>
-			</div>
-		</div>
-		<div>
-			<p>今月:<b>{ monthP.toFixed(1) }%</b></p>
-			<div class="meter">
-				<div class="val" style={ 'width:' + monthP + '%' }></div>
-			</div>
-		</div>
-		<div>
-			<p>今年:<b>{ yearP.toFixed(1) }%</b></p>
-			<div class="meter">
-				<div class="val" style={ 'width:' + yearP + '%' }></div>
-			</div>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding 16px 0
-			color #777
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			&[data-special='on-new-years-day']
-				border-color #ef95a0
-
-			&[data-melt]
-				background transparent
-				border none
-
-			&:after
-				content ""
-				display block
-				clear both
-
-			> .calendar
-				float left
-				width 60%
-				text-align center
-
-				&[data-is-holiday]
-					> .day
-						color #ef95a0
-
-				> p
-					margin 0
-					line-height 18px
-					font-size 14px
-
-					> span
-						margin 0 4px
-
-				> .day
-					margin 10px 0
-					line-height 32px
-					font-size 28px
-
-			> .info
-				display block
-				float left
-				width 40%
-				padding 0 16px 0 0
-
-				> div
-					margin-bottom 8px
-
-					&:last-child
-						margin-bottom 4px
-
-					> p
-						margin 0 0 2px 0
-						font-size 12px
-						line-height 18px
-						color #888
-
-						> b
-							margin-left 2px
-
-					> .meter
-						width 100%
-						overflow hidden
-						background #eee
-						border-radius 8px
-
-						> .val
-							height 4px
-							background $theme-color
-
-					&:nth-child(1)
-						> .meter > .val
-							background #f7796c
-
-					&:nth-child(2)
-						> .meter > .val
-							background #a1de41
-
-					&:nth-child(3)
-						> .meter > .val
-							background #41ddde
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			design: 0
-		};
-
-		this.mixin('widget');
-
-		this.draw = () => {
-			const now = new Date();
-			const nd = now.getDate();
-			const nm = now.getMonth();
-			const ny = now.getFullYear();
-
-			this.year = ny;
-			this.month = nm + 1;
-			this.day = nd;
-			this.weekDay = ['日', '月', '火', '水', '木', '金', '土'][now.getDay()];
-
-			this.dayNumer   = now - new Date(ny, nm, nd);
-			this.dayDenom   = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/;
-			this.monthNumer = now - new Date(ny, nm, 1);
-			this.monthDenom = new Date(ny, nm + 1, 1) - new Date(ny, nm, 1);
-			this.yearNumer  = now - new Date(ny, 0, 1);
-			this.yearDenom  = new Date(ny + 1, 0, 1) - new Date(ny, 0, 1);
-
-			this.dayP   = this.dayNumer   / this.dayDenom   * 100;
-			this.monthP = this.monthNumer / this.monthDenom * 100;
-			this.yearP  = this.yearNumer  / this.yearDenom  * 100;
-
-			this.isHoliday = now.getDay() == 0 || now.getDay() == 6;
-
-			this.special =
-				nm == 0 && nd == 1 ? 'on-new-years-day' :
-				false;
-
-			this.update();
-		};
-
-		this.draw();
-
-		this.on('mount', () => {
-			this.clock = setInterval(this.draw, 1000);
-		});
-
-		this.on('unmount', () => {
-			clearInterval(this.clock);
-		});
-
-		this.func = () => {
-			if (++this.data.design == 2) this.data.design = 0;
-			this.save();
-		};
-	</script>
-</mk-calendar-home-widget>
diff --git a/src/web/app/desktop/-tags/home-widgets/donation.tag b/src/web/app/desktop/-tags/home-widgets/donation.tag
deleted file mode 100644
index 5ed5c137b..000000000
--- a/src/web/app/desktop/-tags/home-widgets/donation.tag
+++ /dev/null
@@ -1,36 +0,0 @@
-<mk-donation-home-widget>
-	<article>
-		<h1>%fa:heart%%i18n:desktop.tags.mk-donation-home-widget.title%</h1>
-		<p>{'%i18n:desktop.tags.mk-donation-home-widget.text%'.substr(0, '%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('{'))}<a href="/syuilo" data-user-preview="@syuilo">@syuilo</a>{'%i18n:desktop.tags.mk-donation-home-widget.text%'.substr('%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('}') + 1)}</p>
-	</article>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px #ead8bb
-			border-radius 6px
-
-			> article
-				padding 20px
-
-				> h1
-					margin 0 0 5px 0
-					font-size 1em
-					color #888
-
-					> [data-fa]
-						margin-right 0.25em
-
-				> p
-					display block
-					z-index 1
-					margin 0
-					font-size 0.8em
-					color #999
-
-	</style>
-	<script lang="typescript">
-		this.mixin('widget');
-		this.mixin('user-preview');
-	</script>
-</mk-donation-home-widget>
diff --git a/src/web/app/desktop/-tags/home-widgets/messaging.tag b/src/web/app/desktop/-tags/home-widgets/messaging.tag
deleted file mode 100644
index d3b77b58c..000000000
--- a/src/web/app/desktop/-tags/home-widgets/messaging.tag
+++ /dev/null
@@ -1,52 +0,0 @@
-<mk-messaging-home-widget>
-	<template v-if="data.design == 0">
-		<p class="title">%fa:comments%%i18n:desktop.tags.mk-messaging-home-widget.title%</p>
-	</template>
-	<mk-messaging ref="index" compact={ true }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow hidden
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> .title
-				z-index 2
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> [data-fa]
-					margin-right 4px
-
-			> mk-messaging
-				max-height 250px
-				overflow auto
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			design: 0
-		};
-
-		this.mixin('widget');
-
-		this.on('mount', () => {
-			this.$refs.index.on('navigate-user', user => {
-				riot.mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
-					user: user
-				});
-			});
-		});
-
-		this.func = () => {
-			if (++this.data.design == 2) this.data.design = 0;
-			this.save();
-		};
-	</script>
-</mk-messaging-home-widget>
diff --git a/src/web/app/desktop/-tags/home-widgets/nav.tag b/src/web/app/desktop/-tags/home-widgets/nav.tag
deleted file mode 100644
index 890fb4d8f..000000000
--- a/src/web/app/desktop/-tags/home-widgets/nav.tag
+++ /dev/null
@@ -1,23 +0,0 @@
-<mk-nav-home-widget>
-	<mk-nav-links/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding 16px
-			font-size 12px
-			color #aaa
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			a
-				color #999
-
-			i
-				color #ccc
-
-	</style>
-	<script lang="typescript">
-		this.mixin('widget');
-	</script>
-</mk-nav-home-widget>
diff --git a/src/web/app/desktop/-tags/home-widgets/photo-stream.tag b/src/web/app/desktop/-tags/home-widgets/photo-stream.tag
deleted file mode 100644
index a2d95dede..000000000
--- a/src/web/app/desktop/-tags/home-widgets/photo-stream.tag
+++ /dev/null
@@ -1,118 +0,0 @@
-<mk-photo-stream-home-widget data-melt={ data.design == 2 }>
-	<template v-if="data.design == 0">
-		<p class="title">%fa:camera%%i18n:desktop.tags.mk-photo-stream-home-widget.title%</p>
-	</template>
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<div class="stream" v-if="!initializing && images.length > 0">
-		<template each={ image in images }>
-			<div class="img" style={ 'background-image: url(' + image.url + '?thumbnail&size=256)' }></div>
-		</template>
-	</div>
-	<p class="empty" v-if="!initializing && images.length == 0">%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			&[data-melt]
-				background transparent !important
-				border none !important
-
-				> .stream
-					padding 0
-
-					> .img
-						border solid 4px transparent
-						border-radius 8px
-
-			> .title
-				z-index 1
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> [data-fa]
-					margin-right 4px
-
-			> .stream
-				display -webkit-flex
-				display -moz-flex
-				display -ms-flex
-				display flex
-				justify-content center
-				flex-wrap wrap
-				padding 8px
-
-				> .img
-					flex 1 1 33%
-					width 33%
-					height 80px
-					background-position center center
-					background-size cover
-					border solid 2px transparent
-					border-radius 4px
-
-			> .initializing
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			design: 0
-		};
-
-		this.mixin('widget');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.images = [];
-		this.initializing = true;
-
-		this.on('mount', () => {
-			this.connection.on('drive_file_created', this.onStreamDriveFileCreated);
-
-			this.api('drive/stream', {
-				type: 'image/*',
-				limit: 9
-			}).then(images => {
-				this.update({
-					initializing: false,
-					images: images
-				});
-			});
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('drive_file_created', this.onStreamDriveFileCreated);
-			this.stream.dispose(this.connectionId);
-		});
-
-		this.onStreamDriveFileCreated = file => {
-			if (/^image\/.+$/.test(file.type)) {
-				this.images.unshift(file);
-				if (this.images.length > 9) this.images.pop();
-				this.update();
-			}
-		};
-
-		this.func = () => {
-			if (++this.data.design == 3) this.data.design = 0;
-			this.save();
-		};
-	</script>
-</mk-photo-stream-home-widget>
diff --git a/src/web/app/desktop/-tags/home-widgets/slideshow.tag b/src/web/app/desktop/-tags/home-widgets/slideshow.tag
deleted file mode 100644
index a69ab74b7..000000000
--- a/src/web/app/desktop/-tags/home-widgets/slideshow.tag
+++ /dev/null
@@ -1,151 +0,0 @@
-<mk-slideshow-home-widget>
-	<div @click="choose">
-		<p v-if="data.folder === undefined">クリックしてフォルダを指定してください</p>
-		<p v-if="data.folder !== undefined && images.length == 0 && !fetching">このフォルダには画像がありません</p>
-		<div ref="slideA" class="slide a"></div>
-		<div ref="slideB" class="slide b"></div>
-	</div>
-	<button @click="resize">%fa:expand%</button>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow hidden
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			&:hover > button
-				display block
-
-			> button
-				position absolute
-				left 0
-				bottom 0
-				display none
-				padding 4px
-				font-size 24px
-				color #fff
-				text-shadow 0 0 8px #000
-
-			> div
-				width 100%
-				height 100%
-				cursor pointer
-
-				> *
-					pointer-events none
-
-				> .slide
-					position absolute
-					top 0
-					left 0
-					width 100%
-					height 100%
-					background-size cover
-					background-position center
-
-					&.b
-						opacity 0
-
-	</style>
-	<script lang="typescript">
-		import * as anime from 'animejs';
-
-		this.data = {
-			folder: undefined,
-			size: 0
-		};
-
-		this.mixin('widget');
-
-		this.images = [];
-		this.fetching = true;
-
-		this.on('mount', () => {
-			this.applySize();
-
-			if (this.data.folder !== undefined) {
-				this.fetch();
-			}
-
-			this.clock = setInterval(this.change, 10000);
-		});
-
-		this.on('unmount', () => {
-			clearInterval(this.clock);
-		});
-
-		this.applySize = () => {
-			let h;
-
-			if (this.data.size == 1) {
-				h = 250;
-			} else {
-				h = 170;
-			}
-
-			this.root.style.height = `${h}px`;
-		};
-
-		this.resize = () => {
-			this.data.size++;
-			if (this.data.size == 2) this.data.size = 0;
-
-			this.applySize();
-			this.save();
-		};
-
-		this.change = () => {
-			if (this.images.length == 0) return;
-
-			const index = Math.floor(Math.random() * this.images.length);
-			const img = `url(${ this.images[index].url }?thumbnail&size=1024)`;
-
-			this.$refs.slideB.style.backgroundImage = img;
-
-			anime({
-				targets: this.$refs.slideB,
-				opacity: 1,
-				duration: 1000,
-				easing: 'linear',
-				complete: () => {
-					this.$refs.slideA.style.backgroundImage = img;
-					anime({
-						targets: this.$refs.slideB,
-						opacity: 0,
-						duration: 0
-					});
-				}
-			});
-		};
-
-		this.fetch = () => {
-			this.update({
-				fetching: true
-			});
-
-			this.api('drive/files', {
-				folder_id: this.data.folder,
-				type: 'image/*',
-				limit: 100
-			}).then(images => {
-				this.update({
-					fetching: false,
-					images: images
-				});
-				this.$refs.slideA.style.backgroundImage = '';
-				this.$refs.slideB.style.backgroundImage = '';
-				this.change();
-			});
-		};
-
-		this.choose = () => {
-			const i = riot.mount(document.body.appendChild(document.createElement('mk-select-folder-from-drive-window')))[0];
-			i.one('selected', folder => {
-				this.data.folder = folder ? folder.id : null;
-				this.fetch();
-				this.save();
-			});
-		};
-	</script>
-</mk-slideshow-home-widget>
diff --git a/src/web/app/desktop/-tags/home-widgets/tips.tag b/src/web/app/desktop/-tags/home-widgets/tips.tag
deleted file mode 100644
index efe9c90fc..000000000
--- a/src/web/app/desktop/-tags/home-widgets/tips.tag
+++ /dev/null
@@ -1,94 +0,0 @@
-<mk-tips-home-widget>
-	<p ref="tip">%fa:R lightbulb%<span ref="text"></span></p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow visible !important
-
-			> p
-				display block
-				margin 0
-				padding 0 12px
-				text-align center
-				font-size 0.7em
-				color #999
-
-				> [data-fa]
-					margin-right 4px
-
-				kbd
-					display inline
-					padding 0 6px
-					margin 0 2px
-					font-size 1em
-					font-family inherit
-					border solid 1px #999
-					border-radius 2px
-
-	</style>
-	<script lang="typescript">
-		import * as anime from 'animejs';
-
-		this.mixin('widget');
-
-		this.tips = [
-			'<kbd>t</kbd>でタイムラインにフォーカスできます',
-			'<kbd>p</kbd>または<kbd>n</kbd>で投稿フォームを開きます',
-			'投稿フォームにはファイルをドラッグ&ドロップできます',
-			'投稿フォームにクリップボードにある画像データをペーストできます',
-			'ドライブにファイルをドラッグ&ドロップしてアップロードできます',
-			'ドライブでファイルをドラッグしてフォルダ移動できます',
-			'ドライブでフォルダをドラッグしてフォルダ移動できます',
-			'ホームは設定からカスタマイズできます',
-			'MisskeyはMIT Licenseです',
-			'タイムマシンウィジェットを利用すると、簡単に過去のタイムラインに遡れます',
-			'投稿の ... をクリックして、投稿をユーザーページにピン留めできます',
-			'ドライブの容量は(デフォルトで)1GBです',
-			'投稿に添付したファイルは全てドライブに保存されます',
-			'ホームのカスタマイズ中、ウィジェットを右クリックしてデザインを変更できます',
-			'タイムライン上部にもウィジェットを設置できます',
-			'投稿をダブルクリックすると詳細が見れます',
-			'「**」でテキストを囲むと**強調表示**されます',
-			'チャンネルウィジェットを利用すると、よく利用するチャンネルを素早く確認できます',
-			'いくつかのウィンドウはブラウザの外に切り離すことができます',
-			'カレンダーウィジェットのパーセンテージは、経過の割合を示しています',
-			'APIを利用してbotの開発なども行えます',
-			'MisskeyはLINEを通じてでも利用できます',
-			'まゆかわいいよまゆ',
-			'Misskeyは2014年にサービスを開始しました',
-			'対応ブラウザではMisskeyを開いていなくても通知を受け取れます'
-		]
-
-		this.on('mount', () => {
-			this.set();
-			this.clock = setInterval(this.change, 20000);
-		});
-
-		this.on('unmount', () => {
-			clearInterval(this.clock);
-		});
-
-		this.set = () => {
-			this.$refs.text.innerHTML = this.tips[Math.floor(Math.random() * this.tips.length)];
-		};
-
-		this.change = () => {
-			anime({
-				targets: this.$refs.tip,
-				opacity: 0,
-				duration: 500,
-				easing: 'linear',
-				complete: this.set
-			});
-
-			setTimeout(() => {
-				anime({
-					targets: this.$refs.tip,
-					opacity: 1,
-					duration: 500,
-					easing: 'linear'
-				});
-			}, 500);
-		};
-	</script>
-</mk-tips-home-widget>
diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
index 9850db485..d97f78155 100644
--- a/webpack/plugins/index.ts
+++ b/webpack/plugins/index.ts
@@ -11,11 +11,11 @@ const isProduction = env === 'production';
 export default (version, lang) => {
 	const plugins = [
 		consts(lang),
-		new StringReplacePlugin(),
-		hoist()
+		new StringReplacePlugin()
 	];
 
 	if (isProduction) {
+		plugins.push(hoist());
 		plugins.push(minify());
 	}
 

From a2c247e53f36000db6113d309966db8db1a9d263 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 12:44:42 +0900
Subject: [PATCH 117/286] wip

---
 webpack/module/rules/vue.ts | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/webpack/module/rules/vue.ts b/webpack/module/rules/vue.ts
index 0d38b4deb..02d644615 100644
--- a/webpack/module/rules/vue.ts
+++ b/webpack/module/rules/vue.ts
@@ -5,5 +5,9 @@
 export default () => ({
 	test: /\.vue$/,
 	exclude: /node_modules/,
-	loader: 'vue-loader'
+	loader: 'vue-loader',
+	options: {
+		cssSourceMap: false,
+		preserveWhitespace: false
+	}
 });

From 28e692985c7bb2c20bdfc0aba044e4c037b00fc5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 13:18:34 +0900
Subject: [PATCH 118/286] wip

---
 webpack/webpack.config.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 4386de3db..1a516d141 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -39,6 +39,7 @@ module.exports = Object.keys(langs).map(lang => {
 			extensions: [
 				'.js', '.ts'
 			]
-		}
+		},
+		cache: true
 	};
 });

From 17861ce6d18f93a10540de7a4c544b7591c8bb91 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 13:24:46 +0900
Subject: [PATCH 119/286] wip

---
 src/web/app/desktop/-tags/list-user.tag       |  93 ----------------
 .../desktop/views/components/list-user.vue    | 101 ++++++++++++++++++
 2 files changed, 101 insertions(+), 93 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/list-user.tag
 create mode 100644 src/web/app/desktop/views/components/list-user.vue

diff --git a/src/web/app/desktop/-tags/list-user.tag b/src/web/app/desktop/-tags/list-user.tag
deleted file mode 100644
index bde90b1cc..000000000
--- a/src/web/app/desktop/-tags/list-user.tag
+++ /dev/null
@@ -1,93 +0,0 @@
-<mk-list-user>
-	<a class="avatar-anchor" href={ '/' + user.username }>
-		<img class="avatar" src={ user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-	</a>
-	<div class="main">
-		<header>
-			<a class="name" href={ '/' + user.username }>{ user.name }</a>
-			<span class="username">@{ user.username }</span>
-		</header>
-		<div class="body">
-			<p class="followed" v-if="user.is_followed">フォローされています</p>
-			<div class="description">{ user.description }</div>
-		</div>
-	</div>
-	<mk-follow-button user={ user }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 16px
-			font-size 16px
-
-			&:after
-				content ""
-				display block
-				clear both
-
-			> .avatar-anchor
-				display block
-				float left
-				margin 0 16px 0 0
-
-				> .avatar
-					display block
-					width 58px
-					height 58px
-					margin 0
-					border-radius 8px
-					vertical-align bottom
-
-			> .main
-				float left
-				width calc(100% - 74px)
-
-				> header
-					margin-bottom 2px
-
-					> .name
-						display inline
-						margin 0
-						padding 0
-						color #777
-						font-size 1em
-						font-weight 700
-						text-align left
-						text-decoration none
-
-						&:hover
-							text-decoration underline
-
-					> .username
-						text-align left
-						margin 0 0 0 8px
-						color #ccc
-
-				> .body
-					> .followed
-						display inline-block
-						margin 0 0 4px 0
-						padding 2px 8px
-						vertical-align top
-						font-size 10px
-						color #71afc7
-						background #eefaff
-						border-radius 4px
-
-					> .description
-						cursor default
-						display block
-						margin 0
-						padding 0
-						overflow-wrap break-word
-						font-size 1.1em
-						color #717171
-
-			> mk-follow-button
-				position absolute
-				top 16px
-				right 16px
-
-	</style>
-	<script lang="typescript">this.user = this.opts.user</script>
-</mk-list-user>
diff --git a/src/web/app/desktop/views/components/list-user.vue b/src/web/app/desktop/views/components/list-user.vue
new file mode 100644
index 000000000..28304e475
--- /dev/null
+++ b/src/web/app/desktop/views/components/list-user.vue
@@ -0,0 +1,101 @@
+<template>
+<div class="mk-list-user">
+	<a class="avatar-anchor" :href="`/${user.username}`">
+		<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+	</a>
+	<div class="main">
+		<header>
+			<a class="name" :href="`/${user.username}`">{{ user.name }}</a>
+			<span class="username">@{{ user.username }}</span>
+		</header>
+		<div class="body">
+			<p class="followed" v-if="user.is_followed">フォローされています</p>
+			<div class="description">{{ user.description }}</div>
+		</div>
+	</div>
+	<mk-follow-button :user="user"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user']
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-list-user
+	margin 0
+	padding 16px
+	font-size 16px
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	> .avatar-anchor
+		display block
+		float left
+		margin 0 16px 0 0
+
+		> .avatar
+			display block
+			width 58px
+			height 58px
+			margin 0
+			border-radius 8px
+			vertical-align bottom
+
+	> .main
+		float left
+		width calc(100% - 74px)
+
+		> header
+			margin-bottom 2px
+
+			> .name
+				display inline
+				margin 0
+				padding 0
+				color #777
+				font-size 1em
+				font-weight 700
+				text-align left
+				text-decoration none
+
+				&:hover
+					text-decoration underline
+
+			> .username
+				text-align left
+				margin 0 0 0 8px
+				color #ccc
+
+		> .body
+			> .followed
+				display inline-block
+				margin 0 0 4px 0
+				padding 2px 8px
+				vertical-align top
+				font-size 10px
+				color #71afc7
+				background #eefaff
+				border-radius 4px
+
+			> .description
+				cursor default
+				display block
+				margin 0
+				padding 0
+				overflow-wrap break-word
+				font-size 1.1em
+				color #717171
+
+	> mk-follow-button
+		position absolute
+		top 16px
+		right 16px
+
+</style>

From 0add64bffd47a5c9954fd374db668a06bd7850b1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 13:35:46 +0900
Subject: [PATCH 120/286] wip

---
 src/web/app/mobile/tags/sub-post-content.tag  | 46 -------------------
 src/web/app/mobile/views/sub-post-content.vue | 43 +++++++++++++++++
 2 files changed, 43 insertions(+), 46 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/sub-post-content.tag
 create mode 100644 src/web/app/mobile/views/sub-post-content.vue

diff --git a/src/web/app/mobile/tags/sub-post-content.tag b/src/web/app/mobile/tags/sub-post-content.tag
deleted file mode 100644
index 211f59171..000000000
--- a/src/web/app/mobile/tags/sub-post-content.tag
+++ /dev/null
@@ -1,46 +0,0 @@
-<mk-sub-post-content>
-	<div class="body"><a class="reply" v-if="post.reply_id">%fa:reply%</a><span ref="text"></span><a class="quote" v-if="post.repost_id" href={ '/post:' + post.repost_id }>RP: ...</a></div>
-	<details v-if="post.media">
-		<summary>({ post.media.length }個のメディア)</summary>
-		<mk-images images={ post.media }/>
-	</details>
-	<details v-if="post.poll">
-		<summary>%i18n:mobile.tags.mk-sub-post-content.poll%</summary>
-		<mk-poll post={ post }/>
-	</details>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow-wrap break-word
-
-			> .body
-				> .reply
-					margin-right 6px
-					color #717171
-
-				> .quote
-					margin-left 4px
-					font-style oblique
-					color #a0bf46
-
-			mk-poll
-				font-size 80%
-
-	</style>
-	<script lang="typescript">
-		import compile from '../../common/scripts/text-compiler';
-
-		this.post = this.opts.post;
-
-		this.on('mount', () => {
-			if (this.post.text) {
-				const tokens = this.post.ast;
-				this.$refs.text.innerHTML = compile(tokens, false);
-
-				Array.from(this.$refs.text.children).forEach(e => {
-					if (e.tagName == 'MK-URL') riot.mount(e);
-				});
-			}
-		});
-	</script>
-</mk-sub-post-content>
diff --git a/src/web/app/mobile/views/sub-post-content.vue b/src/web/app/mobile/views/sub-post-content.vue
new file mode 100644
index 000000000..e3e059f16
--- /dev/null
+++ b/src/web/app/mobile/views/sub-post-content.vue
@@ -0,0 +1,43 @@
+<template>
+<div class="mk-sub-post-content">
+	<div class="body">
+		<a class="reply" v-if="post.reply_id">%fa:reply%</a>
+		<mk-post-html v-if="post.ast" :ast="post.ast" :i="$root.$data.os.i"/>
+		<a class="quote" v-if="post.repost_id" href={ '/post:' + post.repost_id }>RP: ...</a>
+	</div>
+	<details v-if="post.media">
+		<summary>({ post.media.length }個のメディア)</summary>
+		<mk-images images={ post.media }/>
+	</details>
+	<details v-if="post.poll">
+		<summary>%i18n:mobile.tags.mk-sub-post-content.poll%</summary>
+		<mk-poll :post="post"/>
+	</details>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['post']
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-sub-post-content
+	overflow-wrap break-word
+
+	> .body
+		> .reply
+			margin-right 6px
+			color #717171
+
+		> .quote
+			margin-left 4px
+			font-style oblique
+			color #a0bf46
+
+	mk-poll
+		font-size 80%
+
+</style>

From 1f2be831e9cee6277d0a0d2bd9553565c02a9a5c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 13:36:53 +0900
Subject: [PATCH 121/286] wip

---
 src/web/app/mobile/views/sub-post-content.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/web/app/mobile/views/sub-post-content.vue b/src/web/app/mobile/views/sub-post-content.vue
index e3e059f16..48f3791aa 100644
--- a/src/web/app/mobile/views/sub-post-content.vue
+++ b/src/web/app/mobile/views/sub-post-content.vue
@@ -3,11 +3,11 @@
 	<div class="body">
 		<a class="reply" v-if="post.reply_id">%fa:reply%</a>
 		<mk-post-html v-if="post.ast" :ast="post.ast" :i="$root.$data.os.i"/>
-		<a class="quote" v-if="post.repost_id" href={ '/post:' + post.repost_id }>RP: ...</a>
+		<a class="quote" v-if="post.repost_id">RP: ...</a>
 	</div>
 	<details v-if="post.media">
-		<summary>({ post.media.length }個のメディア)</summary>
-		<mk-images images={ post.media }/>
+		<summary>({{ post.media.length }}個のメディア)</summary>
+		<mk-images :images="post.media"/>
 	</details>
 	<details v-if="post.poll">
 		<summary>%i18n:mobile.tags.mk-sub-post-content.poll%</summary>

From b9cb703f972ae055b0dde4c241a23ab941a7d8f3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 13:42:21 +0900
Subject: [PATCH 122/286] wip

---
 src/web/app/mobile/tags/post-preview.tag  | 94 ---------------------
 src/web/app/mobile/views/post-preview.vue | 99 +++++++++++++++++++++++
 2 files changed, 99 insertions(+), 94 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/post-preview.tag
 create mode 100644 src/web/app/mobile/views/post-preview.vue

diff --git a/src/web/app/mobile/tags/post-preview.tag b/src/web/app/mobile/tags/post-preview.tag
deleted file mode 100644
index 3389bf1f0..000000000
--- a/src/web/app/mobile/tags/post-preview.tag
+++ /dev/null
@@ -1,94 +0,0 @@
-<mk-post-preview>
-	<article>
-		<a class="avatar-anchor" href={ '/' + post.user.username }>
-			<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		</a>
-		<div class="main">
-			<header>
-				<a class="name" href={ '/' + post.user.username }>{ post.user.name }</a>
-				<span class="username">@{ post.user.username }</span>
-				<a class="time" href={ '/' + post.user.username + '/' + post.id }>
-					<mk-time time={ post.created_at }/>
-				</a>
-			</header>
-			<div class="body">
-				<mk-sub-post-content class="text" post={ post }/>
-			</div>
-		</div>
-	</article>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 0
-			font-size 0.9em
-			background #fff
-
-			> article
-				&:after
-					content ""
-					display block
-					clear both
-
-				&:hover
-					> .main > footer > button
-						color #888
-
-				> .avatar-anchor
-					display block
-					float left
-					margin 0 12px 0 0
-
-					> .avatar
-						display block
-						width 48px
-						height 48px
-						margin 0
-						border-radius 8px
-						vertical-align bottom
-
-				> .main
-					float left
-					width calc(100% - 60px)
-
-					> header
-						display flex
-						margin-bottom 4px
-						white-space nowrap
-
-						> .name
-							display block
-							margin 0 .5em 0 0
-							padding 0
-							overflow hidden
-							color #607073
-							font-size 1em
-							font-weight 700
-							text-align left
-							text-decoration none
-							text-overflow ellipsis
-
-							&:hover
-								text-decoration underline
-
-						> .username
-							text-align left
-							margin 0 .5em 0 0
-							color #d1d8da
-
-						> .time
-							margin-left auto
-							color #b2b8bb
-
-					> .body
-
-						> .text
-							cursor default
-							margin 0
-							padding 0
-							font-size 1.1em
-							color #717171
-
-	</style>
-	<script lang="typescript">this.post = this.opts.post</script>
-</mk-post-preview>
diff --git a/src/web/app/mobile/views/post-preview.vue b/src/web/app/mobile/views/post-preview.vue
new file mode 100644
index 000000000..ccb8b5f33
--- /dev/null
+++ b/src/web/app/mobile/views/post-preview.vue
@@ -0,0 +1,99 @@
+<template>
+<div class="mk-post-preview">
+	<a class="avatar-anchor" :href="`/${post.user.username}`">
+		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+	</a>
+	<div class="main">
+		<header>
+			<a class="name" :href="`/${post.user.username}`">{{ post.user.name }}</a>
+			<span class="username">@{{ post.user.username }}</span>
+			<a class="time" :href="`/${post.user.username}/${post.id}`">
+				<mk-time :time="post.created_at"/>
+			</a>
+		</header>
+		<div class="body">
+			<mk-sub-post-content class="text" :post="post"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['post']
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-post-preview
+	margin 0
+	padding 0
+	font-size 0.9em
+	background #fff
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	&:hover
+		> .main > footer > button
+			color #888
+
+	> .avatar-anchor
+		display block
+		float left
+		margin 0 12px 0 0
+
+		> .avatar
+			display block
+			width 48px
+			height 48px
+			margin 0
+			border-radius 8px
+			vertical-align bottom
+
+	> .main
+		float left
+		width calc(100% - 60px)
+
+		> header
+			display flex
+			margin-bottom 4px
+			white-space nowrap
+
+			> .name
+				display block
+				margin 0 .5em 0 0
+				padding 0
+				overflow hidden
+				color #607073
+				font-size 1em
+				font-weight 700
+				text-align left
+				text-decoration none
+				text-overflow ellipsis
+
+				&:hover
+					text-decoration underline
+
+			> .username
+				text-align left
+				margin 0 .5em 0 0
+				color #d1d8da
+
+			> .time
+				margin-left auto
+				color #b2b8bb
+
+		> .body
+
+			> .text
+				cursor default
+				margin 0
+				padding 0
+				font-size 1.1em
+				color #717171
+
+</style>

From ff67563dc8e7c7f96a0286607cf2dc981814df92 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 15:14:28 +0900
Subject: [PATCH 123/286] wip

---
 .../views/components/images.vue               | 17 ++--
 .../desktop/views/components/images-image.vue | 18 ++--
 src/web/app/mobile/tags/images.tag            | 82 -------------------
 .../views/{ => components}/friends-maker.vue  |  0
 .../mobile/views/components/images-image.vue  | 37 +++++++++
 .../views/{ => components}/post-form.vue      |  0
 .../views/{ => components}/post-preview.vue   |  0
 .../views/{ => components}/posts-post-sub.vue |  0
 .../views/{ => components}/posts-post.vue     |  0
 .../mobile/views/{ => components}/posts.vue   |  0
 .../{ => components}/sub-post-content.vue     |  0
 .../views/{ => components}/timeline.vue       |  0
 .../views/{ => components}/ui-header.vue      |  0
 .../mobile/views/{ => components}/ui-nav.vue  |  0
 .../app/mobile/views/{ => components}/ui.vue  |  0
 .../views/{ => components}/user-card.vue      |  0
 16 files changed, 57 insertions(+), 97 deletions(-)
 rename src/web/app/{desktop => common}/views/components/images.vue (97%)
 delete mode 100644 src/web/app/mobile/tags/images.tag
 rename src/web/app/mobile/views/{ => components}/friends-maker.vue (100%)
 create mode 100644 src/web/app/mobile/views/components/images-image.vue
 rename src/web/app/mobile/views/{ => components}/post-form.vue (100%)
 rename src/web/app/mobile/views/{ => components}/post-preview.vue (100%)
 rename src/web/app/mobile/views/{ => components}/posts-post-sub.vue (100%)
 rename src/web/app/mobile/views/{ => components}/posts-post.vue (100%)
 rename src/web/app/mobile/views/{ => components}/posts.vue (100%)
 rename src/web/app/mobile/views/{ => components}/sub-post-content.vue (100%)
 rename src/web/app/mobile/views/{ => components}/timeline.vue (100%)
 rename src/web/app/mobile/views/{ => components}/ui-header.vue (100%)
 rename src/web/app/mobile/views/{ => components}/ui-nav.vue (100%)
 rename src/web/app/mobile/views/{ => components}/ui.vue (100%)
 rename src/web/app/mobile/views/{ => components}/user-card.vue (100%)

diff --git a/src/web/app/desktop/views/components/images.vue b/src/web/app/common/views/components/images.vue
similarity index 97%
rename from src/web/app/desktop/views/components/images.vue
rename to src/web/app/common/views/components/images.vue
index f02ecbaa8..dc802a018 100644
--- a/src/web/app/desktop/views/components/images.vue
+++ b/src/web/app/common/views/components/images.vue
@@ -4,13 +4,6 @@
 </div>
 </template>
 
-<style lang="stylus" scoped>
-.mk-images
-	display grid
-	grid-gap 4px
-	height 256px
-</style>
-
 <script lang="ts">
 import Vue from 'vue';
 
@@ -58,3 +51,13 @@ export default Vue.extend({
 	}
 });
 </script>
+
+<style lang="stylus" scoped>
+.mk-images
+	display grid
+	grid-gap 4px
+	height 256px
+
+	@media (max-width 500px)
+		height 192px
+</style>
diff --git a/src/web/app/desktop/views/components/images-image.vue b/src/web/app/desktop/views/components/images-image.vue
index 5ef8ffcda..b29428ac3 100644
--- a/src/web/app/desktop/views/components/images-image.vue
+++ b/src/web/app/desktop/views/components/images-image.vue
@@ -1,11 +1,14 @@
 <template>
-<a class="mk-images-image"
-	:href="image.url"
-	@mousemove="onMousemove"
-	@mouseleave="onMouseleave"
-	@click.prevent="onClick"
-	:style="style"
-	:title="image.name"></a>
+<div>
+	<a class="mk-images-image"
+		:href="image.url"
+		@mousemove="onMousemove"
+		@mouseleave="onMouseleave"
+		@click.prevent="onClick"
+		:style="style"
+		:title="image.name"
+	></a>
+</div>
 </template>
 
 <script lang="ts">
@@ -50,7 +53,6 @@ export default Vue.extend({
 
 <style lang="stylus" scoped>
 .mk-images-image
-	display block
 	overflow hidden
 	border-radius 4px
 
diff --git a/src/web/app/mobile/tags/images.tag b/src/web/app/mobile/tags/images.tag
deleted file mode 100644
index 7d95d6de2..000000000
--- a/src/web/app/mobile/tags/images.tag
+++ /dev/null
@@ -1,82 +0,0 @@
-<mk-images>
-	<template each={ image in images }>
-		<mk-images-image image={ image }/>
-	</template>
-	<style lang="stylus" scoped>
-		:scope
-			display grid
-			grid-gap 4px
-			height 256px
-
-			@media (max-width 500px)
-				height 192px
-	</style>
-	<script lang="typescript">
-		this.images = this.opts.images;
-
-		this.on('mount', () => {
-			if (this.images.length == 1) {
-				this.root.style.gridTemplateRows = '1fr';
-
-				this.tags['mk-images-image'].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-image'].root.style.gridRow = '1 / 2';
-			} else if (this.images.length == 2) {
-				this.root.style.gridTemplateColumns = '1fr 1fr';
-				this.root.style.gridTemplateRows = '1fr';
-
-				this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
-			} else if (this.images.length == 3) {
-				this.root.style.gridTemplateColumns = '1fr 0.5fr';
-				this.root.style.gridTemplateRows = '1fr 1fr';
-
-				this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-image'][0].root.style.gridRow = '1 / 3';
-				this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-image'][2].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3';
-			} else if (this.images.length == 4) {
-				this.root.style.gridTemplateColumns = '1fr 1fr';
-				this.root.style.gridTemplateRows = '1fr 1fr';
-
-				this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
-				this.tags['mk-images-image'][2].root.style.gridColumn = '1 / 2';
-				this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3';
-				this.tags['mk-images-image'][3].root.style.gridColumn = '2 / 3';
-				this.tags['mk-images-image'][3].root.style.gridRow = '2 / 3';
-			}
-		});
-	</script>
-</mk-images>
-
-<mk-images-image>
-	<a ref="view" href={ image.url } target="_blank" style={ styles } title={ image.name }></a>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow hidden
-			border-radius 4px
-
-			> a
-				display block
-				overflow hidden
-				width 100%
-				height 100%
-				background-position center
-				background-size cover
-
-	</style>
-	<script lang="typescript">
-		this.image = this.opts.image;
-		this.styles = {
-			'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
-			'background-image': `url(${this.image.url}?thumbnail&size=512)`
-		};
-	</script>
-</mk-images-image>
diff --git a/src/web/app/mobile/views/friends-maker.vue b/src/web/app/mobile/views/components/friends-maker.vue
similarity index 100%
rename from src/web/app/mobile/views/friends-maker.vue
rename to src/web/app/mobile/views/components/friends-maker.vue
diff --git a/src/web/app/mobile/views/components/images-image.vue b/src/web/app/mobile/views/components/images-image.vue
new file mode 100644
index 000000000..e89923492
--- /dev/null
+++ b/src/web/app/mobile/views/components/images-image.vue
@@ -0,0 +1,37 @@
+<template>
+<div>
+	<a class="mk-images-image" :href="image.url" target="_blank" :style="style" :title="image.name"></a>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['image'],
+	computed: {
+		style(): any {
+			return {
+				'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
+				'background-image': `url(${this.image.url}?thumbnail&size=512)`
+			};
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-images-image
+	display block
+	overflow hidden
+	border-radius 4px
+
+	> a
+		display block
+		overflow hidden
+		width 100%
+		height 100%
+		background-position center
+		background-size cover
+
+</style>
diff --git a/src/web/app/mobile/views/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
similarity index 100%
rename from src/web/app/mobile/views/post-form.vue
rename to src/web/app/mobile/views/components/post-form.vue
diff --git a/src/web/app/mobile/views/post-preview.vue b/src/web/app/mobile/views/components/post-preview.vue
similarity index 100%
rename from src/web/app/mobile/views/post-preview.vue
rename to src/web/app/mobile/views/components/post-preview.vue
diff --git a/src/web/app/mobile/views/posts-post-sub.vue b/src/web/app/mobile/views/components/posts-post-sub.vue
similarity index 100%
rename from src/web/app/mobile/views/posts-post-sub.vue
rename to src/web/app/mobile/views/components/posts-post-sub.vue
diff --git a/src/web/app/mobile/views/posts-post.vue b/src/web/app/mobile/views/components/posts-post.vue
similarity index 100%
rename from src/web/app/mobile/views/posts-post.vue
rename to src/web/app/mobile/views/components/posts-post.vue
diff --git a/src/web/app/mobile/views/posts.vue b/src/web/app/mobile/views/components/posts.vue
similarity index 100%
rename from src/web/app/mobile/views/posts.vue
rename to src/web/app/mobile/views/components/posts.vue
diff --git a/src/web/app/mobile/views/sub-post-content.vue b/src/web/app/mobile/views/components/sub-post-content.vue
similarity index 100%
rename from src/web/app/mobile/views/sub-post-content.vue
rename to src/web/app/mobile/views/components/sub-post-content.vue
diff --git a/src/web/app/mobile/views/timeline.vue b/src/web/app/mobile/views/components/timeline.vue
similarity index 100%
rename from src/web/app/mobile/views/timeline.vue
rename to src/web/app/mobile/views/components/timeline.vue
diff --git a/src/web/app/mobile/views/ui-header.vue b/src/web/app/mobile/views/components/ui-header.vue
similarity index 100%
rename from src/web/app/mobile/views/ui-header.vue
rename to src/web/app/mobile/views/components/ui-header.vue
diff --git a/src/web/app/mobile/views/ui-nav.vue b/src/web/app/mobile/views/components/ui-nav.vue
similarity index 100%
rename from src/web/app/mobile/views/ui-nav.vue
rename to src/web/app/mobile/views/components/ui-nav.vue
diff --git a/src/web/app/mobile/views/ui.vue b/src/web/app/mobile/views/components/ui.vue
similarity index 100%
rename from src/web/app/mobile/views/ui.vue
rename to src/web/app/mobile/views/components/ui.vue
diff --git a/src/web/app/mobile/views/user-card.vue b/src/web/app/mobile/views/components/user-card.vue
similarity index 100%
rename from src/web/app/mobile/views/user-card.vue
rename to src/web/app/mobile/views/components/user-card.vue

From 413f518f32fd5af80bfd51e43aed86107e0013ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 15:20:27 +0900
Subject: [PATCH 124/286] wip

---
 src/web/app/mobile/tags/index.ts | 50 --------------------------------
 1 file changed, 50 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/index.ts

diff --git a/src/web/app/mobile/tags/index.ts b/src/web/app/mobile/tags/index.ts
deleted file mode 100644
index 20934cdd8..000000000
--- a/src/web/app/mobile/tags/index.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-require('./ui.tag');
-require('./page/entrance.tag');
-require('./page/entrance/signin.tag');
-require('./page/entrance/signup.tag');
-require('./page/home.tag');
-require('./page/drive.tag');
-require('./page/notifications.tag');
-require('./page/user.tag');
-require('./page/user-followers.tag');
-require('./page/user-following.tag');
-require('./page/post.tag');
-require('./page/new-post.tag');
-require('./page/search.tag');
-require('./page/settings.tag');
-require('./page/settings/profile.tag');
-require('./page/settings/signin.tag');
-require('./page/settings/authorized-apps.tag');
-require('./page/settings/twitter.tag');
-require('./page/messaging.tag');
-require('./page/messaging-room.tag');
-require('./page/selectdrive.tag');
-require('./home.tag');
-require('./home-timeline.tag');
-require('./timeline.tag');
-require('./post-preview.tag');
-require('./sub-post-content.tag');
-require('./images.tag');
-require('./drive.tag');
-require('./drive-selector.tag');
-require('./drive-folder-selector.tag');
-require('./drive/file.tag');
-require('./drive/folder.tag');
-require('./drive/file-viewer.tag');
-require('./post-form.tag');
-require('./notification.tag');
-require('./notifications.tag');
-require('./notify.tag');
-require('./notification-preview.tag');
-require('./search.tag');
-require('./search-posts.tag');
-require('./post-detail.tag');
-require('./user.tag');
-require('./user-timeline.tag');
-require('./follow-button.tag');
-require('./user-preview.tag');
-require('./users-list.tag');
-require('./user-following.tag');
-require('./user-followers.tag');
-require('./init-following.tag');
-require('./user-card.tag');

From c5feaf5913041f37ee9b06031e1621ac8ebb15fa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 15:37:25 +0900
Subject: [PATCH 125/286] wip

---
 src/web/app/mobile/tags/follow-button.tag     | 131 ------------------
 .../mobile/views/components/follow-button.vue | 121 ++++++++++++++++
 2 files changed, 121 insertions(+), 131 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/follow-button.tag
 create mode 100644 src/web/app/mobile/views/components/follow-button.vue

diff --git a/src/web/app/mobile/tags/follow-button.tag b/src/web/app/mobile/tags/follow-button.tag
deleted file mode 100644
index c6215a7ba..000000000
--- a/src/web/app/mobile/tags/follow-button.tag
+++ /dev/null
@@ -1,131 +0,0 @@
-<mk-follow-button>
-	<button :class="{ wait: wait, follow: !user.is_following, unfollow: user.is_following }" v-if="!init" @click="onclick" disabled={ wait }>
-		<template v-if="!wait && user.is_following">%fa:minus%</template>
-		<template v-if="!wait && !user.is_following">%fa:plus%</template>
-		<template v-if="wait">%fa:spinner .pulse .fw%</template>{ user.is_following ? '%i18n:mobile.tags.mk-follow-button.unfollow%' : '%i18n:mobile.tags.mk-follow-button.follow%' }
-	</button>
-	<div class="init" v-if="init">%fa:spinner .pulse .fw%</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> button
-			> .init
-				display block
-				user-select none
-				cursor pointer
-				padding 0 16px
-				margin 0
-				height inherit
-				font-size 16px
-				outline none
-				border solid 1px $theme-color
-				border-radius 4px
-
-				*
-					pointer-events none
-
-				&.follow
-					color $theme-color
-					background transparent
-
-					&:hover
-						background rgba($theme-color, 0.1)
-
-					&:active
-						background rgba($theme-color, 0.2)
-
-				&.unfollow
-					color $theme-color-foreground
-					background $theme-color
-
-				&.wait
-					cursor wait !important
-					opacity 0.7
-
-				&.init
-					cursor wait !important
-					opacity 0.7
-
-				> [data-fa]
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		import isPromise from '../../common/scripts/is-promise';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.user = null;
-		this.userPromise = isPromise(this.opts.user)
-			? this.opts.user
-			: Promise.resolve(this.opts.user);
-		this.init = true;
-		this.wait = false;
-
-		this.on('mount', () => {
-			this.userPromise.then(user => {
-				this.update({
-					init: false,
-					user: user
-				});
-				this.connection.on('follow', this.onStreamFollow);
-				this.connection.on('unfollow', this.onStreamUnfollow);
-			});
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('follow', this.onStreamFollow);
-			this.connection.off('unfollow', this.onStreamUnfollow);
-			this.stream.dispose(this.connectionId);
-		});
-
-		this.onStreamFollow = user => {
-			if (user.id == this.user.id) {
-				this.update({
-					user: user
-				});
-			}
-		};
-
-		this.onStreamUnfollow = user => {
-			if (user.id == this.user.id) {
-				this.update({
-					user: user
-				});
-			}
-		};
-
-		this.onclick = () => {
-			this.wait = true;
-			if (this.user.is_following) {
-				this.api('following/delete', {
-					user_id: this.user.id
-				}).then(() => {
-					this.user.is_following = false;
-				}).catch(err => {
-					console.error(err);
-				}).then(() => {
-					this.wait = false;
-					this.update();
-				});
-			} else {
-				this.api('following/create', {
-					user_id: this.user.id
-				}).then(() => {
-					this.user.is_following = true;
-				}).catch(err => {
-					console.error(err);
-				}).then(() => {
-					this.wait = false;
-					this.update();
-				});
-			}
-		};
-	</script>
-</mk-follow-button>
diff --git a/src/web/app/mobile/views/components/follow-button.vue b/src/web/app/mobile/views/components/follow-button.vue
new file mode 100644
index 000000000..455be388c
--- /dev/null
+++ b/src/web/app/mobile/views/components/follow-button.vue
@@ -0,0 +1,121 @@
+<template>
+<button class="mk-follow-button"
+	:class="{ wait: wait, follow: !user.is_following, unfollow: user.is_following }"
+	@click="onClick"
+	:disabled="wait"
+>
+	<template v-if="!wait && user.is_following">%fa:minus%</template>
+	<template v-if="!wait && !user.is_following">%fa:plus%</template>
+	<template v-if="wait">%fa:spinner .pulse .fw%</template>
+	{{ user.is_following ? '%i18n:mobile.tags.mk-follow-button.unfollow%' : '%i18n:mobile.tags.mk-follow-button.follow%' }}
+</button>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		user: {
+			type: Object,
+			required: true
+		}
+	},
+	data() {
+		return {
+			wait: false,
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		this.connection = this.$root.$data.os.stream.getConnection();
+		this.connectionId = this.$root.$data.os.stream.use();
+
+		this.connection.on('follow', this.onFollow);
+		this.connection.on('unfollow', this.onUnfollow);
+	},
+	beforeDestroy() {
+		this.connection.off('follow', this.onFollow);
+		this.connection.off('unfollow', this.onUnfollow);
+		this.$root.$data.os.stream.dispose(this.connectionId);
+	},
+	methods: {
+
+		onFollow(user) {
+			if (user.id == this.user.id) {
+				this.user.is_following = user.is_following;
+			}
+		},
+
+		onUnfollow(user) {
+			if (user.id == this.user.id) {
+				this.user.is_following = user.is_following;
+			}
+		},
+
+		onClick() {
+			this.wait = true;
+			if (this.user.is_following) {
+				this.api('following/delete', {
+					user_id: this.user.id
+				}).then(() => {
+					this.user.is_following = false;
+				}).catch(err => {
+					console.error(err);
+				}).then(() => {
+					this.wait = false;
+				});
+			} else {
+				this.api('following/create', {
+					user_id: this.user.id
+				}).then(() => {
+					this.user.is_following = true;
+				}).catch(err => {
+					console.error(err);
+				}).then(() => {
+					this.wait = false;
+				});
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-follow-button
+	display block
+	user-select none
+	cursor pointer
+	padding 0 16px
+	margin 0
+	height inherit
+	font-size 16px
+	outline none
+	border solid 1px $theme-color
+	border-radius 4px
+
+	*
+		pointer-events none
+
+	&.follow
+		color $theme-color
+		background transparent
+
+		&:hover
+			background rgba($theme-color, 0.1)
+
+		&:active
+			background rgba($theme-color, 0.2)
+
+	&.unfollow
+		color $theme-color-foreground
+		background $theme-color
+
+	&.wait
+		cursor wait !important
+		opacity 0.7
+
+	> [data-fa]
+		margin-right 4px
+
+</style>

From f207d8c6c5fb2ca00c0ad1f15ef369a76e35658a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 16:48:53 +0900
Subject: [PATCH 126/286] wip

---
 src/web/app/mobile/tags/notification.tag      | 169 ----------------
 .../mobile/views/components/notification.vue  | 189 ++++++++++++++++++
 2 files changed, 189 insertions(+), 169 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/notification.tag
 create mode 100644 src/web/app/mobile/views/components/notification.vue

diff --git a/src/web/app/mobile/tags/notification.tag b/src/web/app/mobile/tags/notification.tag
deleted file mode 100644
index c942e21aa..000000000
--- a/src/web/app/mobile/tags/notification.tag
+++ /dev/null
@@ -1,169 +0,0 @@
-<mk-notification :class="{ notification.type }">
-	<mk-time time={ notification.created_at }/>
-	<template v-if="notification.type == 'reaction'">
-		<a class="avatar-anchor" href={ '/' + notification.user.username }>
-			<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		</a>
-		<div class="text">
-			<p>
-				<mk-reaction-icon reaction={ notification.reaction }/>
-				<a href={ '/' + notification.user.username }>{ notification.user.name }</a>
-			</p>
-			<a class="post-ref" href={ '/' + notification.post.user.username + '/' + notification.post.id }>
-				%fa:quote-left%{ getPostSummary(notification.post) }%fa:quote-right%
-			</a>
-		</div>
-	</template>
-	<template v-if="notification.type == 'repost'">
-		<a class="avatar-anchor" href={ '/' + notification.post.user.username }>
-			<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		</a>
-		<div class="text">
-			<p>
-				%fa:retweet%
-				<a href={ '/' + notification.post.user.username }>{ notification.post.user.name }</a>
-			</p>
-			<a class="post-ref" href={ '/' + notification.post.user.username + '/' + notification.post.id }>
-				%fa:quote-left%{ getPostSummary(notification.post.repost) }%fa:quote-right%
-			</a>
-		</div>
-	</template>
-	<template v-if="notification.type == 'quote'">
-		<a class="avatar-anchor" href={ '/' + notification.post.user.username }>
-			<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		</a>
-		<div class="text">
-			<p>
-				%fa:quote-left%
-				<a href={ '/' + notification.post.user.username }>{ notification.post.user.name }</a>
-			</p>
-			<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
-		</div>
-	</template>
-	<template v-if="notification.type == 'follow'">
-		<a class="avatar-anchor" href={ '/' + notification.user.username }>
-			<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		</a>
-		<div class="text">
-			<p>
-				%fa:user-plus%
-				<a href={ '/' + notification.user.username }>{ notification.user.name }</a>
-			</p>
-		</div>
-	</template>
-	<template v-if="notification.type == 'reply'">
-		<a class="avatar-anchor" href={ '/' + notification.post.user.username }>
-			<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		</a>
-		<div class="text">
-			<p>
-				%fa:reply%
-				<a href={ '/' + notification.post.user.username }>{ notification.post.user.name }</a>
-			</p>
-			<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
-		</div>
-	</template>
-	<template v-if="notification.type == 'mention'">
-		<a class="avatar-anchor" href={ '/' + notification.post.user.username }>
-			<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		</a>
-		<div class="text">
-			<p>
-				%fa:at%
-				<a href={ '/' + notification.post.user.username }>{ notification.post.user.name }</a>
-			</p>
-			<a class="post-preview" href={ '/' + notification.post.user.username + '/' + notification.post.id }>{ getPostSummary(notification.post) }</a>
-		</div>
-	</template>
-	<template v-if="notification.type == 'poll_vote'">
-		<a class="avatar-anchor" href={ '/' + notification.user.username }>
-			<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		</a>
-		<div class="text">
-			<p>
-				%fa:chart-pie%
-				<a href={ '/' + notification.user.username }>{ notification.user.name }</a>
-			</p>
-			<a class="post-ref" href={ '/' + notification.post.user.username + '/' + notification.post.id }>
-				%fa:quote-left%{ getPostSummary(notification.post) }%fa:quote-right%
-			</a>
-		</div>
-	</template>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 16px
-			overflow-wrap break-word
-
-			> mk-time
-				display inline
-				position absolute
-				top 16px
-				right 12px
-				vertical-align top
-				color rgba(0, 0, 0, 0.6)
-				font-size 12px
-
-			&:after
-				content ""
-				display block
-				clear both
-
-			.avatar-anchor
-				display block
-				float left
-
-				img
-					min-width 36px
-					min-height 36px
-					max-width 36px
-					max-height 36px
-					border-radius 6px
-
-			.text
-				float right
-				width calc(100% - 36px)
-				padding-left 8px
-
-				p
-					margin 0
-
-					i, mk-reaction-icon
-						margin-right 4px
-
-			.post-preview
-				color rgba(0, 0, 0, 0.7)
-
-			.post-ref
-				color rgba(0, 0, 0, 0.7)
-
-				[data-fa]
-					font-size 1em
-					font-weight normal
-					font-style normal
-					display inline-block
-					margin-right 3px
-
-			&.repost, &.quote
-				.text p i
-					color #77B255
-
-			&.follow
-				.text p i
-					color #53c7ce
-
-			&.reply, &.mention
-				.text p i
-					color #555
-
-				.post-preview
-					color rgba(0, 0, 0, 0.7)
-
-	</style>
-	<script lang="typescript">
-		import getPostSummary from '../../../../common/get-post-summary.ts';
-		this.getPostSummary = getPostSummary;
-		this.notification = this.opts.notification;
-	</script>
-</mk-notification>
diff --git a/src/web/app/mobile/views/components/notification.vue b/src/web/app/mobile/views/components/notification.vue
new file mode 100644
index 000000000..dca672941
--- /dev/null
+++ b/src/web/app/mobile/views/components/notification.vue
@@ -0,0 +1,189 @@
+<template>
+<div class="mk-notification" :class="notification.type">
+	<mk-time :time="notification.created_at"/>
+
+	<template v-if="notification.type == 'reaction'">
+		<a class="avatar-anchor" :href="`/${notification.user.username}`">
+			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		</a>
+		<div class="text">
+			<p>
+				<mk-reaction-icon :reaction="notification.reaction"/>
+				<a :href="`/${notification.user.username}`">{{ notification.user.name }}</a>
+			</p>
+			<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
+				%fa:quote-left%{{ getPostSummary(notification.post) }}
+				%fa:quote-right%
+			</a>
+		</div>
+	</template>
+
+	<template v-if="notification.type == 'repost'">
+		<a class="avatar-anchor" :href="`/${notification.post.user.username}`">
+			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		</a>
+		<div class="text">
+			<p>
+				%fa:retweet%
+				<a :href="`/${notification.post.user.username}`">{{ notification.post.user.name }}</a>
+			</p>
+			<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
+				%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
+			</a>
+		</div>
+	</template>
+
+	<template v-if="notification.type == 'quote'">
+		<a class="avatar-anchor" :href="`/${notification.post.user.username}`">
+			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		</a>
+		<div class="text">
+			<p>
+				%fa:quote-left%
+				<a :href="`/${notification.post.user.username}`">{{ notification.post.user.name }}</a>
+			</p>
+			<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
+		</div>
+	</template>
+
+	<template v-if="notification.type == 'follow'">
+		<a class="avatar-anchor" :href="`/${notification.user.username}`">
+			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		</a>
+		<div class="text">
+			<p>
+				%fa:user-plus%
+				<a :href="`/${notification.user.username}`">{{ notification.user.name }}</a>
+			</p>
+		</div>
+	</template>
+
+	<template v-if="notification.type == 'reply'">
+		<a class="avatar-anchor" :href="`/${notification.post.user.username}`">
+			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		</a>
+		<div class="text">
+			<p>
+				%fa:reply%
+				<a :href="`/${notification.post.user.username}`">{{ notification.post.user.name }}</a>
+			</p>
+			<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
+		</div>
+	</template>
+
+	<template v-if="notification.type == 'mention'">
+		<a class="avatar-anchor" :href="`/${notification.post.user.username}`">
+			<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		</a>
+		<div class="text">
+			<p>
+				%fa:at%
+				<a :href="`/${notification.post.user.username}`">{{ notification.post.user.name }}</a>
+			</p>
+			<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
+		</div>
+	</template>
+
+	<template v-if="notification.type == 'poll_vote'">
+		<a class="avatar-anchor" :href="`/${notification.user.username}`">
+			<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		</a>
+		<div class="text">
+			<p>
+				%fa:chart-pie%
+				<a :href="`/${notification.user.username}`">{{ notification.user.name }}</a>
+			</p>
+			<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
+				%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
+			</a>
+		</div>
+	</template>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import getPostSummary from '../../../../../common/get-post-summary';
+
+export default Vue.extend({
+	data() {
+		return {
+			getPostSummary
+		};
+	}
+});
+</script>
+
+
+<style lang="stylus" scoped>
+.mk-notification
+	margin 0
+	padding 16px
+	overflow-wrap break-word
+
+	> mk-time
+		display inline
+		position absolute
+		top 16px
+		right 12px
+		vertical-align top
+		color rgba(0, 0, 0, 0.6)
+		font-size 12px
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	.avatar-anchor
+		display block
+		float left
+
+		img
+			min-width 36px
+			min-height 36px
+			max-width 36px
+			max-height 36px
+			border-radius 6px
+
+	.text
+		float right
+		width calc(100% - 36px)
+		padding-left 8px
+
+		p
+			margin 0
+
+			i, mk-reaction-icon
+				margin-right 4px
+
+	.post-preview
+		color rgba(0, 0, 0, 0.7)
+
+	.post-ref
+		color rgba(0, 0, 0, 0.7)
+
+		[data-fa]
+			font-size 1em
+			font-weight normal
+			font-style normal
+			display inline-block
+			margin-right 3px
+
+	&.repost, &.quote
+		.text p i
+			color #77B255
+
+	&.follow
+		.text p i
+			color #53c7ce
+
+	&.reply, &.mention
+		.text p i
+			color #555
+
+		.post-preview
+			color rgba(0, 0, 0, 0.7)
+
+</style>
+

From 31f77d182bfc1f847bfc783bdadc21a052d97f49 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 17:08:48 +0900
Subject: [PATCH 127/286] wip

---
 src/web/app/mobile/tags/notifications.tag     | 164 -----------------
 .../mobile/views/components/notification.vue  |   1 -
 .../mobile/views/components/notifications.vue | 167 ++++++++++++++++++
 3 files changed, 167 insertions(+), 165 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/notifications.tag
 create mode 100644 src/web/app/mobile/views/components/notifications.vue

diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
deleted file mode 100644
index 8a1482aca..000000000
--- a/src/web/app/mobile/tags/notifications.tag
+++ /dev/null
@@ -1,164 +0,0 @@
-<mk-notifications>
-	<div class="notifications" v-if="notifications.length != 0">
-		<template each={ notification, i in notifications }>
-			<mk-notification notification={ notification }/>
-			<p class="date" v-if="i != notifications.length - 1 && notification._date != notifications[i + 1]._date"><span>%fa:angle-up%{ notification._datetext }</span><span>%fa:angle-down%{ notifications[i + 1]._datetext }</span></p>
-		</template>
-	</div>
-	<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" disabled={ fetchingMoreNotifications }>
-		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-notifications.more%' }
-	</button>
-	<p class="empty" v-if="notifications.length == 0 && !loading">%i18n:mobile.tags.mk-notifications.empty%</p>
-	<p class="loading" v-if="loading">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 8px auto
-			padding 0
-			max-width 500px
-			width calc(100% - 16px)
-			background #fff
-			border-radius 8px
-			box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
-
-			@media (min-width 500px)
-				margin 16px auto
-				width calc(100% - 32px)
-
-			> .notifications
-
-				> mk-notification
-					margin 0 auto
-					max-width 500px
-					border-bottom solid 1px rgba(0, 0, 0, 0.05)
-
-					&:last-child
-						border-bottom none
-
-				> .date
-					display block
-					margin 0
-					line-height 32px
-					text-align center
-					font-size 0.8em
-					color #aaa
-					background #fdfdfd
-					border-bottom solid 1px rgba(0, 0, 0, 0.05)
-
-					span
-						margin 0 16px
-
-					i
-						margin-right 8px
-
-			> .more
-				display block
-				width 100%
-				padding 16px
-				color #555
-				border-top solid 1px rgba(0, 0, 0, 0.05)
-
-				> [data-fa]
-					margin-right 4px
-
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-			> .loading
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		import getPostSummary from '../../../../common/get-post-summary.ts';
-		this.getPostSummary = getPostSummary;
-
-		this.mixin('api');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.notifications = [];
-		this.loading = true;
-
-		this.on('mount', () => {
-			const max = 10;
-
-			this.api('i/notifications', {
-				limit: max + 1
-			}).then(notifications => {
-				if (notifications.length == max + 1) {
-					this.moreNotifications = true;
-					notifications.pop();
-				}
-
-				this.update({
-					loading: false,
-					notifications: notifications
-				});
-
-				this.$emit('fetched');
-			});
-
-			this.connection.on('notification', this.onNotification);
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('notification', this.onNotification);
-			this.stream.dispose(this.connectionId);
-		});
-
-		this.on('update', () => {
-			this.notifications.forEach(notification => {
-				const date = new Date(notification.created_at).getDate();
-				const month = new Date(notification.created_at).getMonth() + 1;
-				notification._date = date;
-				notification._datetext = `${month}月 ${date}日`;
-			});
-		});
-
-		this.onNotification = notification => {
-			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
-			this.connection.send({
-				type: 'read_notification',
-				id: notification.id
-			});
-
-			this.notifications.unshift(notification);
-			this.update();
-		};
-
-		this.fetchMoreNotifications = () => {
-			this.update({
-				fetchingMoreNotifications: true
-			});
-
-			const max = 30;
-
-			this.api('i/notifications', {
-				limit: max + 1,
-				until_id: this.notifications[this.notifications.length - 1].id
-			}).then(notifications => {
-				if (notifications.length == max + 1) {
-					this.moreNotifications = true;
-					notifications.pop();
-				} else {
-					this.moreNotifications = false;
-				}
-				this.update({
-					notifications: this.notifications.concat(notifications),
-					fetchingMoreNotifications: false
-				});
-			});
-		};
-	</script>
-</mk-notifications>
diff --git a/src/web/app/mobile/views/components/notification.vue b/src/web/app/mobile/views/components/notification.vue
index dca672941..1b4608724 100644
--- a/src/web/app/mobile/views/components/notification.vue
+++ b/src/web/app/mobile/views/components/notification.vue
@@ -114,7 +114,6 @@ export default Vue.extend({
 });
 </script>
 
-
 <style lang="stylus" scoped>
 .mk-notification
 	margin 0
diff --git a/src/web/app/mobile/views/components/notifications.vue b/src/web/app/mobile/views/components/notifications.vue
new file mode 100644
index 000000000..cbf8a150f
--- /dev/null
+++ b/src/web/app/mobile/views/components/notifications.vue
@@ -0,0 +1,167 @@
+<template>
+<div class="mk-notifications">
+	<div class="notifications" v-if="notifications.length != 0">
+		<template v-for="(notification, i) in _notifications">
+			<mk-notification :notification="notification" :key="notification.id"/>
+			<p class="date" :key="notification.id + '-time'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date">
+				<span>%fa:angle-up%{ notification._datetext }</span>
+				<span>%fa:angle-down%{ _notifications[i + 1]._datetext }</span>
+			</p>
+		</template>
+	</div>
+	<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" disabled={ fetchingMoreNotifications }>
+		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{ fetchingMoreNotifications ? '%i18n:common.fetching%' : '%i18n:mobile.tags.mk-notifications.more%' }
+	</button>
+	<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:mobile.tags.mk-notifications.empty%</p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.fetching%<mk-ellipsis/></p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			fetching: true,
+			fetchingMoreNotifications: false,
+			notifications: [],
+			moreNotifications: false,
+			connection: null,
+			connectionId: null
+		};
+	},
+	computed: {
+		_notifications(): any[] {
+			return (this.notifications as any).map(notification => {
+				const date = new Date(notification.created_at).getDate();
+				const month = new Date(notification.created_at).getMonth() + 1;
+				notification._date = date;
+				notification._datetext = `${month}月 ${date}日`;
+				return notification;
+			});
+		}
+	},
+	mounted() {
+		this.connection = this.$root.$data.os.stream.getConnection();
+		this.connectionId = this.$root.$data.os.stream.use();
+
+		this.connection.on('notification', this.onNotification);
+
+		const max = 10;
+
+		this.$root.$data.os.api('i/notifications', {
+			limit: max + 1
+		}).then(notifications => {
+			if (notifications.length == max + 1) {
+				this.moreNotifications = true;
+				notifications.pop();
+			}
+
+			this.notifications = notifications;
+			this.fetching = false;
+		});
+	},
+	beforeDestroy() {
+		this.connection.off('notification', this.onNotification);
+		this.$root.$data.os.stream.dispose(this.connectionId);
+	},
+	methods: {
+		fetchMoreNotifications() {
+			this.fetchingMoreNotifications = true;
+
+			const max = 30;
+
+			this.$root.$data.os.api('i/notifications', {
+				limit: max + 1,
+				until_id: this.notifications[this.notifications.length - 1].id
+			}).then(notifications => {
+				if (notifications.length == max + 1) {
+					this.moreNotifications = true;
+					notifications.pop();
+				} else {
+					this.moreNotifications = false;
+				}
+				this.notifications = this.notifications.concat(notifications);
+				this.fetchingMoreNotifications = false;
+			});
+		},
+		onNotification(notification) {
+			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
+			this.connection.send({
+				type: 'read_notification',
+				id: notification.id
+			});
+
+			this.notifications.unshift(notification);
+		}
+	}
+});
+</script>
+
+
+<style lang="stylus" scoped>
+.mk-notifications
+	margin 8px auto
+	padding 0
+	max-width 500px
+	width calc(100% - 16px)
+	background #fff
+	border-radius 8px
+	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+
+	@media (min-width 500px)
+		margin 16px auto
+		width calc(100% - 32px)
+
+	> .notifications
+
+		> mk-notification
+			margin 0 auto
+			max-width 500px
+			border-bottom solid 1px rgba(0, 0, 0, 0.05)
+
+			&:last-child
+				border-bottom none
+
+		> .date
+			display block
+			margin 0
+			line-height 32px
+			text-align center
+			font-size 0.8em
+			color #aaa
+			background #fdfdfd
+			border-bottom solid 1px rgba(0, 0, 0, 0.05)
+
+			span
+				margin 0 16px
+
+			i
+				margin-right 8px
+
+	> .more
+		display block
+		width 100%
+		padding 16px
+		color #555
+		border-top solid 1px rgba(0, 0, 0, 0.05)
+
+		> [data-fa]
+			margin-right 4px
+
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+	> .fetching
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+</style>

From 0766a76348bef2267d619067a5ba51323dadcce2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 17:20:28 +0900
Subject: [PATCH 128/286] wip

---
 .../app/mobile/tags/notification-preview.tag  | 110 ---------------
 .../views/components/notification-preview.vue | 128 ++++++++++++++++++
 .../mobile/views/components/notifications.vue |   1 -
 3 files changed, 128 insertions(+), 111 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/notification-preview.tag
 create mode 100644 src/web/app/mobile/views/components/notification-preview.vue

diff --git a/src/web/app/mobile/tags/notification-preview.tag b/src/web/app/mobile/tags/notification-preview.tag
deleted file mode 100644
index bc37f198e..000000000
--- a/src/web/app/mobile/tags/notification-preview.tag
+++ /dev/null
@@ -1,110 +0,0 @@
-<mk-notification-preview :class="{ notification.type }">
-	<template v-if="notification.type == 'reaction'">
-		<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		<div class="text">
-			<p><mk-reaction-icon reaction={ notification.reaction }/>{ notification.user.name }</p>
-			<p class="post-ref">%fa:quote-left%{ getPostSummary(notification.post) }%fa:quote-right%</p>
-		</div>
-	</template>
-	<template v-if="notification.type == 'repost'">
-		<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		<div class="text">
-			<p>%fa:retweet%{ notification.post.user.name }</p>
-			<p class="post-ref">%fa:quote-left%{ getPostSummary(notification.post.repost) }%fa:quote-right%</p>
-		</div>
-	</template>
-	<template v-if="notification.type == 'quote'">
-		<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		<div class="text">
-			<p>%fa:quote-left%{ notification.post.user.name }</p>
-			<p class="post-preview">{ getPostSummary(notification.post) }</p>
-		</div>
-	</template>
-	<template v-if="notification.type == 'follow'">
-		<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		<div class="text">
-			<p>%fa:user-plus%{ notification.user.name }</p>
-		</div>
-	</template>
-	<template v-if="notification.type == 'reply'">
-		<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		<div class="text">
-			<p>%fa:reply%{ notification.post.user.name }</p>
-			<p class="post-preview">{ getPostSummary(notification.post) }</p>
-		</div>
-	</template>
-	<template v-if="notification.type == 'mention'">
-		<img class="avatar" src={ notification.post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		<div class="text">
-			<p>%fa:at%{ notification.post.user.name }</p>
-			<p class="post-preview">{ getPostSummary(notification.post) }</p>
-		</div>
-	</template>
-	<template v-if="notification.type == 'poll_vote'">
-		<img class="avatar" src={ notification.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		<div class="text">
-			<p>%fa:chart-pie%{ notification.user.name }</p>
-			<p class="post-ref">%fa:quote-left%{ getPostSummary(notification.post) }%fa:quote-right%</p>
-		</div>
-	</template>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 8px
-			color #fff
-			overflow-wrap break-word
-
-			&:after
-				content ""
-				display block
-				clear both
-
-			img
-				display block
-				float left
-				min-width 36px
-				min-height 36px
-				max-width 36px
-				max-height 36px
-				border-radius 6px
-
-			.text
-				float right
-				width calc(100% - 36px)
-				padding-left 8px
-
-				p
-					margin 0
-
-					i, mk-reaction-icon
-						margin-right 4px
-
-			.post-ref
-
-				[data-fa]
-					font-size 1em
-					font-weight normal
-					font-style normal
-					display inline-block
-					margin-right 3px
-
-			&.repost, &.quote
-				.text p i
-					color #77B255
-
-			&.follow
-				.text p i
-					color #53c7ce
-
-			&.reply, &.mention
-				.text p i
-					color #fff
-
-	</style>
-	<script lang="typescript">
-		import getPostSummary from '../../../../common/get-post-summary.ts';
-		this.getPostSummary = getPostSummary;
-		this.notification = this.opts.notification;
-	</script>
-</mk-notification-preview>
diff --git a/src/web/app/mobile/views/components/notification-preview.vue b/src/web/app/mobile/views/components/notification-preview.vue
new file mode 100644
index 000000000..47df626fa
--- /dev/null
+++ b/src/web/app/mobile/views/components/notification-preview.vue
@@ -0,0 +1,128 @@
+<template>
+<div class="mk-notification-preview" :class="notification.type">
+	<template v-if="notification.type == 'reaction'">
+		<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<div class="text">
+			<p><mk-reaction-icon :reaction="notification.reaction"/>{{ notification.user.name }}</p>
+			<p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%</p>
+		</div>
+	</template>
+
+	<template v-if="notification.type == 'repost'">
+		<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<div class="text">
+			<p>%fa:retweet%{{ notification.post.user.name }}</p>
+			<p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%</p>
+		</div>
+	</template>
+
+	<template v-if="notification.type == 'quote'">
+		<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<div class="text">
+			<p>%fa:quote-left%{{ notification.post.user.name }}</p>
+			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
+		</div>
+	</template>
+
+	<template v-if="notification.type == 'follow'">
+		<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<div class="text">
+			<p>%fa:user-plus%{{ notification.user.name }}</p>
+		</div>
+	</template>
+
+	<template v-if="notification.type == 'reply'">
+		<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<div class="text">
+			<p>%fa:reply%{{ notification.post.user.name }}</p>
+			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
+		</div>
+	</template>
+
+	<template v-if="notification.type == 'mention'">
+		<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<div class="text">
+			<p>%fa:at%{{ notification.post.user.name }}</p>
+			<p class="post-preview">{{ getPostSummary(notification.post) }}</p>
+		</div>
+	</template>
+
+	<template v-if="notification.type == 'poll_vote'">
+		<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<div class="text">
+			<p>%fa:chart-pie%{{ notification.user.name }}</p>
+			<p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%</p>
+		</div>
+	</template>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import getPostSummary from '../../../../../common/get-post-summary';
+
+export default Vue.extend({
+	props: ['notification'],
+	data() {
+		return {
+			getPostSummary
+		};
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-notification-preview
+	margin 0
+	padding 8px
+	color #fff
+	overflow-wrap break-word
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	img
+		display block
+		float left
+		min-width 36px
+		min-height 36px
+		max-width 36px
+		max-height 36px
+		border-radius 6px
+
+	.text
+		float right
+		width calc(100% - 36px)
+		padding-left 8px
+
+		p
+			margin 0
+
+			i, mk-reaction-icon
+				margin-right 4px
+
+	.post-ref
+
+		[data-fa]
+			font-size 1em
+			font-weight normal
+			font-style normal
+			display inline-block
+			margin-right 3px
+
+	&.repost, &.quote
+		.text p i
+			color #77B255
+
+	&.follow
+		.text p i
+			color #53c7ce
+
+	&.reply, &.mention
+		.text p i
+			color #fff
+
+</style>
+
diff --git a/src/web/app/mobile/views/components/notifications.vue b/src/web/app/mobile/views/components/notifications.vue
index cbf8a150f..3cad1d514 100644
--- a/src/web/app/mobile/views/components/notifications.vue
+++ b/src/web/app/mobile/views/components/notifications.vue
@@ -98,7 +98,6 @@ export default Vue.extend({
 });
 </script>
 
-
 <style lang="stylus" scoped>
 .mk-notifications
 	margin 8px auto

From 9fd00d11792a9e559aa44bcb4059dc38a19f3731 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 17:24:52 +0900
Subject: [PATCH 129/286] wip

---
 src/web/app/auth/tags/form.tag                |  4 ++--
 src/web/app/auth/tags/index.tag               |  4 ++--
 src/web/app/ch/tags/channel.tag               | 12 +++++------
 src/web/app/ch/tags/index.tag                 |  4 ++--
 src/web/app/common/-tags/activity-table.tag   |  2 +-
 src/web/app/common/-tags/authorized-apps.tag  |  2 +-
 src/web/app/common/-tags/signin-history.tag   |  2 +-
 src/web/app/common/-tags/twitter-setting.tag  |  6 +++---
 src/web/app/common/views/components/poll.vue  |  2 +-
 .../desktop/-tags/autocomplete-suggestion.tag |  2 +-
 .../app/desktop/-tags/big-follow-button.tag   |  4 ++--
 .../desktop/-tags/drive/browser-window.tag    |  2 +-
 .../desktop/-tags/drive/file-contextmenu.tag  |  2 +-
 .../-tags/drive/folder-contextmenu.tag        |  2 +-
 .../desktop/-tags/home-widgets/channel.tag    |  6 +++---
 .../desktop/-tags/home-widgets/mentions.tag   |  4 ++--
 .../desktop/-tags/home-widgets/post-form.tag  |  2 +-
 .../-tags/home-widgets/recommended-polls.tag  |  2 +-
 .../app/desktop/-tags/home-widgets/trends.tag |  2 +-
 .../home-widgets/user-recommendation.tag      |  2 +-
 src/web/app/desktop/-tags/pages/home.tag      |  2 +-
 .../desktop/-tags/pages/messaging-room.tag    |  2 +-
 src/web/app/desktop/-tags/pages/post.tag      |  2 +-
 src/web/app/desktop/-tags/search-posts.tag    |  4 ++--
 src/web/app/desktop/-tags/user-followers.tag  |  2 +-
 src/web/app/desktop/-tags/user-following.tag  |  2 +-
 src/web/app/desktop/-tags/user-preview.tag    |  2 +-
 src/web/app/desktop/-tags/user-timeline.tag   |  4 ++--
 .../app/desktop/-tags/widgets/activity.tag    |  2 +-
 .../views/components/follow-button.vue        |  4 ++--
 src/web/app/dev/tags/new-app-form.tag         |  4 ++--
 src/web/app/dev/tags/pages/app.tag            |  2 +-
 src/web/app/dev/tags/pages/apps.tag           |  2 +-
 src/web/app/mobile/tags/drive.tag             | 20 +++++++++----------
 src/web/app/mobile/tags/drive/file-viewer.tag |  4 ++--
 src/web/app/mobile/tags/page/home.tag         |  2 +-
 .../app/mobile/tags/page/messaging-room.tag   |  2 +-
 .../app/mobile/tags/page/notifications.tag    |  2 +-
 src/web/app/mobile/tags/page/post.tag         |  2 +-
 .../app/mobile/tags/page/settings/profile.tag |  6 +++---
 .../app/mobile/tags/page/user-followers.tag   |  2 +-
 .../app/mobile/tags/page/user-following.tag   |  2 +-
 src/web/app/mobile/tags/post-detail.tag       |  6 +++---
 src/web/app/mobile/tags/search-posts.tag      |  4 ++--
 src/web/app/mobile/tags/user-followers.tag    |  2 +-
 src/web/app/mobile/tags/user-following.tag    |  2 +-
 src/web/app/mobile/tags/user-timeline.tag     |  4 ++--
 src/web/app/mobile/tags/user.tag              | 12 +++++------
 .../mobile/views/components/follow-button.vue |  4 ++--
 src/web/app/stats/tags/index.tag              |  6 +++---
 src/web/app/status/tags/index.tag             |  2 +-
 51 files changed, 93 insertions(+), 93 deletions(-)

diff --git a/src/web/app/auth/tags/form.tag b/src/web/app/auth/tags/form.tag
index 043b6313b..b1de0baab 100644
--- a/src/web/app/auth/tags/form.tag
+++ b/src/web/app/auth/tags/form.tag
@@ -112,7 +112,7 @@
 		this.app = this.session.app;
 
 		this.cancel = () => {
-			this.api('auth/deny', {
+			this.$root.$data.os.api('auth/deny', {
 				token: this.session.token
 			}).then(() => {
 				this.$emit('denied');
@@ -120,7 +120,7 @@
 		};
 
 		this.accept = () => {
-			this.api('auth/accept', {
+			this.$root.$data.os.api('auth/accept', {
 				token: this.session.token
 			}).then(() => {
 				this.$emit('accepted');
diff --git a/src/web/app/auth/tags/index.tag b/src/web/app/auth/tags/index.tag
index e6b1cdb3f..3a24c2d6b 100644
--- a/src/web/app/auth/tags/index.tag
+++ b/src/web/app/auth/tags/index.tag
@@ -96,7 +96,7 @@
 			if (!this.SIGNIN) return;
 
 			// Fetch session
-			this.api('auth/session/show', {
+			this.$root.$data.os.api('auth/session/show', {
 				token: this.token
 			}).then(session => {
 				this.session = session;
@@ -104,7 +104,7 @@
 
 				// 既に連携していた場合
 				if (this.session.app.is_authorized) {
-					this.api('auth/accept', {
+					this.$root.$data.os.api('auth/accept', {
 						token: this.session.token
 					}).then(() => {
 						this.accepted();
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index 524d04270..d95de9737 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -76,7 +76,7 @@
 			let fetched = false;
 
 			// チャンネル概要読み込み
-			this.api('channels/show', {
+			this.$root.$data.os.api('channels/show', {
 				channel_id: this.id
 			}).then(channel => {
 				if (fetched) {
@@ -95,7 +95,7 @@
 			});
 
 			// 投稿読み込み
-			this.api('channels/posts', {
+			this.$root.$data.os.api('channels/posts', {
 				channel_id: this.id
 			}).then(posts => {
 				if (fetched) {
@@ -125,7 +125,7 @@
 			this.posts.unshift(post);
 			this.update();
 
-			if (document.hidden && this.SIGNIN && post.user_id !== this.I.id) {
+			if (document.hidden && this.SIGNIN && post.user_id !== this.$root.$data.os.i.id) {
 				this.unreadCount++;
 				document.title = `(${this.unreadCount}) ${this.channel.title} | Misskey`;
 			}
@@ -139,7 +139,7 @@
 		};
 
 		this.watch = () => {
-			this.api('channels/watch', {
+			this.$root.$data.os.api('channels/watch', {
 				channel_id: this.id
 			}).then(() => {
 				this.channel.is_watching = true;
@@ -150,7 +150,7 @@
 		};
 
 		this.unwatch = () => {
-			this.api('channels/unwatch', {
+			this.$root.$data.os.api('channels/unwatch', {
 				channel_id: this.id
 			}).then(() => {
 				this.channel.is_watching = false;
@@ -323,7 +323,7 @@
 				? this.files.map(f => f.id)
 				: undefined;
 
-			this.api('posts/create', {
+			this.$root.$data.os.api('posts/create', {
 				text: this.$refs.text.value == '' ? undefined : this.$refs.text.value,
 				media_ids: files,
 				reply_id: this.reply ? this.reply.id : undefined,
diff --git a/src/web/app/ch/tags/index.tag b/src/web/app/ch/tags/index.tag
index 6e0b451e8..88df2ec45 100644
--- a/src/web/app/ch/tags/index.tag
+++ b/src/web/app/ch/tags/index.tag
@@ -15,7 +15,7 @@
 		this.mixin('api');
 
 		this.on('mount', () => {
-			this.api('channels', {
+			this.$root.$data.os.api('channels', {
 				limit: 100
 			}).then(channels => {
 				this.update({
@@ -27,7 +27,7 @@
 		this.n = () => {
 			const title = window.prompt('%i18n:ch.tags.mk-index.channel-title%');
 
-			this.api('channels/create', {
+			this.$root.$data.os.api('channels/create', {
 				title: title
 			}).then(channel => {
 				location.href = '/' + channel.id;
diff --git a/src/web/app/common/-tags/activity-table.tag b/src/web/app/common/-tags/activity-table.tag
index 2f716912f..cd74b0920 100644
--- a/src/web/app/common/-tags/activity-table.tag
+++ b/src/web/app/common/-tags/activity-table.tag
@@ -31,7 +31,7 @@
 		this.user = this.opts.user;
 
 		this.on('mount', () => {
-			this.api('aggregation/users/activity', {
+			this.$root.$data.os.api('aggregation/users/activity', {
 				user_id: this.user.id
 			}).then(data => {
 				data.forEach(d => d.total = d.posts + d.replies + d.reposts);
diff --git a/src/web/app/common/-tags/authorized-apps.tag b/src/web/app/common/-tags/authorized-apps.tag
index 26efa1316..288c2fcc2 100644
--- a/src/web/app/common/-tags/authorized-apps.tag
+++ b/src/web/app/common/-tags/authorized-apps.tag
@@ -25,7 +25,7 @@
 		this.fetching = true;
 
 		this.on('mount', () => {
-			this.api('i/authorized_apps').then(apps => {
+			this.$root.$data.os.api('i/authorized_apps').then(apps => {
 				this.apps = apps;
 				this.fetching = false;
 				this.update();
diff --git a/src/web/app/common/-tags/signin-history.tag b/src/web/app/common/-tags/signin-history.tag
index 57ac5ec97..a347c7c23 100644
--- a/src/web/app/common/-tags/signin-history.tag
+++ b/src/web/app/common/-tags/signin-history.tag
@@ -19,7 +19,7 @@
 		this.fetching = true;
 
 		this.on('mount', () => {
-			this.api('i/signin_history').then(history => {
+			this.$root.$data.os.api('i/signin_history').then(history => {
 				this.update({
 					fetching: false,
 					history: history
diff --git a/src/web/app/common/-tags/twitter-setting.tag b/src/web/app/common/-tags/twitter-setting.tag
index 935239f44..a62329083 100644
--- a/src/web/app/common/-tags/twitter-setting.tag
+++ b/src/web/app/common/-tags/twitter-setting.tag
@@ -30,15 +30,15 @@
 		this.form = null;
 
 		this.on('mount', () => {
-			this.I.on('updated', this.onMeUpdated);
+			this.$root.$data.os.i.on('updated', this.onMeUpdated);
 		});
 
 		this.on('unmount', () => {
-			this.I.off('updated', this.onMeUpdated);
+			this.$root.$data.os.i.off('updated', this.onMeUpdated);
 		});
 
 		this.onMeUpdated = () => {
-			if (this.I.twitter) {
+			if (this.$root.$data.os.i.twitter) {
 				if (this.form) this.form.close();
 			}
 		};
diff --git a/src/web/app/common/views/components/poll.vue b/src/web/app/common/views/components/poll.vue
index d85caa00c..19ce557e7 100644
--- a/src/web/app/common/views/components/poll.vue
+++ b/src/web/app/common/views/components/poll.vue
@@ -47,7 +47,7 @@
 			},
 			vote(id) {
 				if (this.poll.choices.some(c => c.is_voted)) return;
-				this.api('posts/polls/vote', {
+				this.$root.$data.os.api('posts/polls/vote', {
 					post_id: this.post.id,
 					choice: id
 				}).then(() => {
diff --git a/src/web/app/desktop/-tags/autocomplete-suggestion.tag b/src/web/app/desktop/-tags/autocomplete-suggestion.tag
index a0215666c..d3c3b6b35 100644
--- a/src/web/app/desktop/-tags/autocomplete-suggestion.tag
+++ b/src/web/app/desktop/-tags/autocomplete-suggestion.tag
@@ -97,7 +97,7 @@
 				el.addEventListener('mousedown', this.mousedown);
 			});
 
-			this.api('users/search_by_username', {
+			this.$root.$data.os.api('users/search_by_username', {
 				query: this.q,
 				limit: 30
 			}).then(users => {
diff --git a/src/web/app/desktop/-tags/big-follow-button.tag b/src/web/app/desktop/-tags/big-follow-button.tag
index 5ea09fdfc..d8222f92c 100644
--- a/src/web/app/desktop/-tags/big-follow-button.tag
+++ b/src/web/app/desktop/-tags/big-follow-button.tag
@@ -126,7 +126,7 @@
 		this.onclick = () => {
 			this.wait = true;
 			if (this.user.is_following) {
-				this.api('following/delete', {
+				this.$root.$data.os.api('following/delete', {
 					user_id: this.user.id
 				}).then(() => {
 					this.user.is_following = false;
@@ -137,7 +137,7 @@
 					this.update();
 				});
 			} else {
-				this.api('following/create', {
+				this.$root.$data.os.api('following/create', {
 					user_id: this.user.id
 				}).then(() => {
 					this.user.is_following = true;
diff --git a/src/web/app/desktop/-tags/drive/browser-window.tag b/src/web/app/desktop/-tags/drive/browser-window.tag
index db7b89834..c9c765252 100644
--- a/src/web/app/desktop/-tags/drive/browser-window.tag
+++ b/src/web/app/desktop/-tags/drive/browser-window.tag
@@ -46,7 +46,7 @@
 				this.$destroy();
 			});
 
-			this.api('drive').then(info => {
+			this.$root.$data.os.api('drive').then(info => {
 				this.update({
 					usage: info.usage / info.capacity * 100
 				});
diff --git a/src/web/app/desktop/-tags/drive/file-contextmenu.tag b/src/web/app/desktop/-tags/drive/file-contextmenu.tag
index 125f70b61..8776fcc02 100644
--- a/src/web/app/desktop/-tags/drive/file-contextmenu.tag
+++ b/src/web/app/desktop/-tags/drive/file-contextmenu.tag
@@ -62,7 +62,7 @@
 			this.$refs.ctx.close();
 
 			inputDialog('%i18n:desktop.tags.mk-drive-browser-file-contextmenu.rename-file%', '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.input-new-file-name%', this.file.name, name => {
-				this.api('drive/files/update', {
+				this.$root.$data.os.api('drive/files/update', {
 					file_id: this.file.id,
 					name: name
 				})
diff --git a/src/web/app/desktop/-tags/drive/folder-contextmenu.tag b/src/web/app/desktop/-tags/drive/folder-contextmenu.tag
index 0cb7f6eb8..a0146410f 100644
--- a/src/web/app/desktop/-tags/drive/folder-contextmenu.tag
+++ b/src/web/app/desktop/-tags/drive/folder-contextmenu.tag
@@ -53,7 +53,7 @@
 			this.$refs.ctx.close();
 
 			inputDialog('%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.rename-folder%', '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.input-new-folder-name%', this.folder.name, name => {
-				this.api('drive/folders/update', {
+				this.$root.$data.os.api('drive/folders/update', {
 					folder_id: this.folder.id,
 					name: name
 				});
diff --git a/src/web/app/desktop/-tags/home-widgets/channel.tag b/src/web/app/desktop/-tags/home-widgets/channel.tag
index 98bf6bf7e..c20a851e7 100644
--- a/src/web/app/desktop/-tags/home-widgets/channel.tag
+++ b/src/web/app/desktop/-tags/home-widgets/channel.tag
@@ -74,7 +74,7 @@
 				fetching: true
 			});
 
-			this.api('channels/show', {
+			this.$root.$data.os.api('channels/show', {
 				channel_id: this.data.channel
 			}).then(channel => {
 				this.update({
@@ -159,7 +159,7 @@
 				channel: channel
 			});
 
-			this.api('channels/posts', {
+			this.$root.$data.os.api('channels/posts', {
 				channel_id: channel.id
 			}).then(posts => {
 				this.update({
@@ -300,7 +300,7 @@
 				text = text.replace(/^>>([0-9]+) /, '');
 			}
 
-			this.api('posts/create', {
+			this.$root.$data.os.api('posts/create', {
 				text: text,
 				reply_id: reply ? reply.id : undefined,
 				channel_id: this.parent.channel.id
diff --git a/src/web/app/desktop/-tags/home-widgets/mentions.tag b/src/web/app/desktop/-tags/home-widgets/mentions.tag
index 81f9b2875..d38ccabb5 100644
--- a/src/web/app/desktop/-tags/home-widgets/mentions.tag
+++ b/src/web/app/desktop/-tags/home-widgets/mentions.tag
@@ -82,7 +82,7 @@
 		};
 
 		this.fetch = cb => {
-			this.api('posts/mentions', {
+			this.$root.$data.os.api('posts/mentions', {
 				following: this.mode == 'following'
 			}).then(posts => {
 				this.update({
@@ -99,7 +99,7 @@
 			this.update({
 				moreLoading: true
 			});
-			this.api('posts/mentions', {
+			this.$root.$data.os.api('posts/mentions', {
 				following: this.mode == 'following',
 				until_id: this.$refs.timeline.tail().id
 			}).then(posts => {
diff --git a/src/web/app/desktop/-tags/home-widgets/post-form.tag b/src/web/app/desktop/-tags/home-widgets/post-form.tag
index d5824477b..8564cdf02 100644
--- a/src/web/app/desktop/-tags/home-widgets/post-form.tag
+++ b/src/web/app/desktop/-tags/home-widgets/post-form.tag
@@ -83,7 +83,7 @@
 				posting: true
 			});
 
-			this.api('posts/create', {
+			this.$root.$data.os.api('posts/create', {
 				text: this.$refs.text.value
 			}).then(data => {
 				this.clear();
diff --git a/src/web/app/desktop/-tags/home-widgets/recommended-polls.tag b/src/web/app/desktop/-tags/home-widgets/recommended-polls.tag
index cfbcd1e92..43c6096a3 100644
--- a/src/web/app/desktop/-tags/home-widgets/recommended-polls.tag
+++ b/src/web/app/desktop/-tags/home-widgets/recommended-polls.tag
@@ -94,7 +94,7 @@
 				loading: true,
 				poll: null
 			});
-			this.api('posts/polls/recommendation', {
+			this.$root.$data.os.api('posts/polls/recommendation', {
 				limit: 1,
 				offset: this.offset
 			}).then(posts => {
diff --git a/src/web/app/desktop/-tags/home-widgets/trends.tag b/src/web/app/desktop/-tags/home-widgets/trends.tag
index 5e297ebc7..9f1be68c7 100644
--- a/src/web/app/desktop/-tags/home-widgets/trends.tag
+++ b/src/web/app/desktop/-tags/home-widgets/trends.tag
@@ -96,7 +96,7 @@
 				loading: true,
 				post: null
 			});
-			this.api('posts/trend', {
+			this.$root.$data.os.api('posts/trend', {
 				limit: 1,
 				offset: this.offset,
 				repost: false,
diff --git a/src/web/app/desktop/-tags/home-widgets/user-recommendation.tag b/src/web/app/desktop/-tags/home-widgets/user-recommendation.tag
index 5344da1f2..bc873539e 100644
--- a/src/web/app/desktop/-tags/home-widgets/user-recommendation.tag
+++ b/src/web/app/desktop/-tags/home-widgets/user-recommendation.tag
@@ -137,7 +137,7 @@
 				loading: true,
 				users: null
 			});
-			this.api('users/recommendation', {
+			this.$root.$data.os.api('users/recommendation', {
 				limit: this.limit,
 				offset: this.limit * this.page
 			}).then(users => {
diff --git a/src/web/app/desktop/-tags/pages/home.tag b/src/web/app/desktop/-tags/pages/home.tag
index 9b9d455b5..83ceb3846 100644
--- a/src/web/app/desktop/-tags/pages/home.tag
+++ b/src/web/app/desktop/-tags/pages/home.tag
@@ -38,7 +38,7 @@
 		});
 
 		this.onStreamPost = post => {
-			if (document.hidden && post.user_id != this.I.id) {
+			if (document.hidden && post.user_id != this.$root.$data.os.i.id) {
 				this.unreadCount++;
 				document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
 			}
diff --git a/src/web/app/desktop/-tags/pages/messaging-room.tag b/src/web/app/desktop/-tags/pages/messaging-room.tag
index bfa8c2465..cfacc4a1b 100644
--- a/src/web/app/desktop/-tags/pages/messaging-room.tag
+++ b/src/web/app/desktop/-tags/pages/messaging-room.tag
@@ -20,7 +20,7 @@
 
 			document.documentElement.style.background = '#fff';
 
-			this.api('users/show', {
+			this.$root.$data.os.api('users/show', {
 				username: this.opts.user
 			}).then(user => {
 				this.update({
diff --git a/src/web/app/desktop/-tags/pages/post.tag b/src/web/app/desktop/-tags/pages/post.tag
index 488adc6e3..baec48c0a 100644
--- a/src/web/app/desktop/-tags/pages/post.tag
+++ b/src/web/app/desktop/-tags/pages/post.tag
@@ -42,7 +42,7 @@
 		this.on('mount', () => {
 			Progress.start();
 
-			this.api('posts/show', {
+			this.$root.$data.os.api('posts/show', {
 				post_id: this.opts.post
 			}).then(post => {
 
diff --git a/src/web/app/desktop/-tags/search-posts.tag b/src/web/app/desktop/-tags/search-posts.tag
index 52c68b754..94a6f2524 100644
--- a/src/web/app/desktop/-tags/search-posts.tag
+++ b/src/web/app/desktop/-tags/search-posts.tag
@@ -48,7 +48,7 @@
 			document.addEventListener('keydown', this.onDocumentKeydown);
 			window.addEventListener('scroll', this.onScroll);
 
-			this.api('posts/search', parse(this.query)).then(posts => {
+			this.$root.$data.os.api('posts/search', parse(this.query)).then(posts => {
 				this.update({
 					isLoading: false,
 					isEmpty: posts.length == 0
@@ -77,7 +77,7 @@
 			this.update({
 				moreLoading: true
 			});
-			return this.api('posts/search', Object.assign({}, parse(this.query), {
+			return this.$root.$data.os.api('posts/search', Object.assign({}, parse(this.query), {
 				limit: this.limit,
 				offset: this.offset
 			})).then(posts => {
diff --git a/src/web/app/desktop/-tags/user-followers.tag b/src/web/app/desktop/-tags/user-followers.tag
index a1b44f0f5..3a5430d37 100644
--- a/src/web/app/desktop/-tags/user-followers.tag
+++ b/src/web/app/desktop/-tags/user-followers.tag
@@ -12,7 +12,7 @@
 		this.user = this.opts.user;
 
 		this.fetch = (iknow, limit, cursor, cb) => {
-			this.api('users/followers', {
+			this.$root.$data.os.api('users/followers', {
 				user_id: this.user.id,
 				iknow: iknow,
 				limit: limit,
diff --git a/src/web/app/desktop/-tags/user-following.tag b/src/web/app/desktop/-tags/user-following.tag
index db46bf110..42ad5f88a 100644
--- a/src/web/app/desktop/-tags/user-following.tag
+++ b/src/web/app/desktop/-tags/user-following.tag
@@ -12,7 +12,7 @@
 		this.user = this.opts.user;
 
 		this.fetch = (iknow, limit, cursor, cb) => {
-			this.api('users/following', {
+			this.$root.$data.os.api('users/following', {
 				user_id: this.user.id,
 				iknow: iknow,
 				limit: limit,
diff --git a/src/web/app/desktop/-tags/user-preview.tag b/src/web/app/desktop/-tags/user-preview.tag
index 18465c224..3a65fb79b 100644
--- a/src/web/app/desktop/-tags/user-preview.tag
+++ b/src/web/app/desktop/-tags/user-preview.tag
@@ -109,7 +109,7 @@
 		this.userPromise =
 			typeof this.u == 'string' ?
 				new Promise((resolve, reject) => {
-					this.api('users/show', {
+					this.$root.$data.os.api('users/show', {
 						user_id: this.u[0] == '@' ? undefined : this.u,
 						username: this.u[0] == '@' ? this.u.substr(1) : undefined
 					}).then(resolve);
diff --git a/src/web/app/desktop/-tags/user-timeline.tag b/src/web/app/desktop/-tags/user-timeline.tag
index f018ba64e..1071b6e2b 100644
--- a/src/web/app/desktop/-tags/user-timeline.tag
+++ b/src/web/app/desktop/-tags/user-timeline.tag
@@ -94,7 +94,7 @@
 		};
 
 		this.fetch = cb => {
-			this.api('users/posts', {
+			this.$root.$data.os.api('users/posts', {
 				user_id: this.user.id,
 				until_date: this.date ? this.date.getTime() : undefined,
 				with_replies: this.mode == 'with-replies'
@@ -113,7 +113,7 @@
 			this.update({
 				moreLoading: true
 			});
-			this.api('users/posts', {
+			this.$root.$data.os.api('users/posts', {
 				user_id: this.user.id,
 				with_replies: this.mode == 'with-replies',
 				until_id: this.$refs.timeline.tail().id
diff --git a/src/web/app/desktop/-tags/widgets/activity.tag b/src/web/app/desktop/-tags/widgets/activity.tag
index 8c20ef5a6..1f9bee5ed 100644
--- a/src/web/app/desktop/-tags/widgets/activity.tag
+++ b/src/web/app/desktop/-tags/widgets/activity.tag
@@ -67,7 +67,7 @@
 		this.initializing = true;
 
 		this.on('mount', () => {
-			this.api('aggregation/users/activity', {
+			this.$root.$data.os.api('aggregation/users/activity', {
 				user_id: this.user.id,
 				limit: 20 * 7
 			}).then(activity => {
diff --git a/src/web/app/desktop/views/components/follow-button.vue b/src/web/app/desktop/views/components/follow-button.vue
index 588bcd641..0fffbda91 100644
--- a/src/web/app/desktop/views/components/follow-button.vue
+++ b/src/web/app/desktop/views/components/follow-button.vue
@@ -57,7 +57,7 @@ export default Vue.extend({
 		onClick() {
 			this.wait = true;
 			if (this.user.is_following) {
-				this.api('following/delete', {
+				this.$root.$data.os.api('following/delete', {
 					user_id: this.user.id
 				}).then(() => {
 					this.user.is_following = false;
@@ -67,7 +67,7 @@ export default Vue.extend({
 					this.wait = false;
 				});
 			} else {
-				this.api('following/create', {
+				this.$root.$data.os.api('following/create', {
 					user_id: this.user.id
 				}).then(() => {
 					this.user.is_following = true;
diff --git a/src/web/app/dev/tags/new-app-form.tag b/src/web/app/dev/tags/new-app-form.tag
index 672c31570..cf3c44007 100644
--- a/src/web/app/dev/tags/new-app-form.tag
+++ b/src/web/app/dev/tags/new-app-form.tag
@@ -209,7 +209,7 @@
 				nidState: 'wait'
 			});
 
-			this.api('app/name_id/available', {
+			this.$root.$data.os.api('app/name_id/available', {
 				name_id: nid
 			}).then(result => {
 				this.update({
@@ -235,7 +235,7 @@
 
 			const locker = document.body.appendChild(document.createElement('mk-locker'));
 
-			this.api('app/create', {
+			this.$root.$data.os.api('app/create', {
 				name: name,
 				name_id: nid,
 				description: description,
diff --git a/src/web/app/dev/tags/pages/app.tag b/src/web/app/dev/tags/pages/app.tag
index 42937a21b..982549ed2 100644
--- a/src/web/app/dev/tags/pages/app.tag
+++ b/src/web/app/dev/tags/pages/app.tag
@@ -19,7 +19,7 @@
 		this.fetching = true;
 
 		this.on('mount', () => {
-			this.api('app/show', {
+			this.$root.$data.os.api('app/show', {
 				app_id: this.opts.app
 			}).then(app => {
 				this.update({
diff --git a/src/web/app/dev/tags/pages/apps.tag b/src/web/app/dev/tags/pages/apps.tag
index bf9552f07..6ae6031e6 100644
--- a/src/web/app/dev/tags/pages/apps.tag
+++ b/src/web/app/dev/tags/pages/apps.tag
@@ -20,7 +20,7 @@
 		this.fetching = true;
 
 		this.on('mount', () => {
-			this.api('my/apps').then(apps => {
+			this.$root.$data.os.api('my/apps').then(apps => {
 				this.fetching = false
 				this.apps = apps
 				this.update({
diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index a7a8a35c3..e0a5872d8 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -265,7 +265,7 @@
 				fetching: true
 			});
 
-			this.api('drive/folders/show', {
+			this.$root.$data.os.api('drive/folders/show', {
 				folder_id: target
 			}).then(folder => {
 				this.folder = folder;
@@ -368,7 +368,7 @@
 			const filesMax = 20;
 
 			// フォルダ一覧取得
-			this.api('drive/folders', {
+			this.$root.$data.os.api('drive/folders', {
 				folder_id: this.folder ? this.folder.id : null,
 				limit: foldersMax + 1
 			}).then(folders => {
@@ -381,7 +381,7 @@
 			});
 
 			// ファイル一覧取得
-			this.api('drive/files', {
+			this.$root.$data.os.api('drive/files', {
 				folder_id: this.folder ? this.folder.id : null,
 				limit: filesMax + 1
 			}).then(files => {
@@ -412,7 +412,7 @@
 
 			if (this.folder == null) {
 				// Fetch addtional drive info
-				this.api('drive').then(info => {
+				this.$root.$data.os.api('drive').then(info => {
 					this.update({ info });
 				});
 			}
@@ -427,7 +427,7 @@
 			const max = 30;
 
 			// ファイル一覧取得
-			this.api('drive/files', {
+			this.$root.$data.os.api('drive/files', {
 				folder_id: this.folder ? this.folder.id : null,
 				limit: max + 1,
 				until_id: this.files[this.files.length - 1].id
@@ -471,7 +471,7 @@
 				fetching: true
 			});
 
-			this.api('drive/files/show', {
+			this.$root.$data.os.api('drive/files/show', {
 				file_id: file
 			}).then(file => {
 				this.fetching = false;
@@ -523,7 +523,7 @@
 		this.createFolder = () => {
 			const name = window.prompt('フォルダー名');
 			if (name == null || name == '') return;
-			this.api('drive/folders/create', {
+			this.$root.$data.os.api('drive/folders/create', {
 				name: name,
 				parent_id: this.folder ? this.folder.id : undefined
 			}).then(folder => {
@@ -539,7 +539,7 @@
 			}
 			const name = window.prompt('フォルダー名', this.folder.name);
 			if (name == null || name == '') return;
-			this.api('drive/folders/update', {
+			this.$root.$data.os.api('drive/folders/update', {
 				name: name,
 				folder_id: this.folder.id
 			}).then(folder => {
@@ -554,7 +554,7 @@
 			}
 			const dialog = riot.mount(document.body.appendChild(document.createElement('mk-drive-folder-selector')))[0];
 			dialog.one('selected', folder => {
-				this.api('drive/folders/update', {
+				this.$root.$data.os.api('drive/folders/update', {
 					parent_id: folder ? folder.id : null,
 					folder_id: this.folder.id
 				}).then(folder => {
@@ -566,7 +566,7 @@
 		this.urlUpload = () => {
 			const url = window.prompt('アップロードしたいファイルのURL');
 			if (url == null || url == '') return;
-			this.api('drive/files/upload_from_url', {
+			this.$root.$data.os.api('drive/files/upload_from_url', {
 				url: url,
 				folder_id: this.folder ? this.folder.id : undefined
 			});
diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag
index ab0c94ae9..e9a89493e 100644
--- a/src/web/app/mobile/tags/drive/file-viewer.tag
+++ b/src/web/app/mobile/tags/drive/file-viewer.tag
@@ -255,7 +255,7 @@
 		this.rename = () => {
 			const name = window.prompt('名前を変更', this.file.name);
 			if (name == null || name == '' || name == this.file.name) return;
-			this.api('drive/files/update', {
+			this.$root.$data.os.api('drive/files/update', {
 				file_id: this.file.id,
 				name: name
 			}).then(() => {
@@ -266,7 +266,7 @@
 		this.move = () => {
 			const dialog = riot.mount(document.body.appendChild(document.createElement('mk-drive-folder-selector')))[0];
 			dialog.one('selected', folder => {
-				this.api('drive/files/update', {
+				this.$root.$data.os.api('drive/files/update', {
 					file_id: this.file.id,
 					folder_id: folder == null ? null : folder.id
 				}).then(() => {
diff --git a/src/web/app/mobile/tags/page/home.tag b/src/web/app/mobile/tags/page/home.tag
index cf57cdb22..10af292f3 100644
--- a/src/web/app/mobile/tags/page/home.tag
+++ b/src/web/app/mobile/tags/page/home.tag
@@ -46,7 +46,7 @@
 		});
 
 		this.onStreamPost = post => {
-			if (document.hidden && post.user_id !== this.I.id) {
+			if (document.hidden && post.user_id !== this.$root.$data.os.i.id) {
 				this.unreadCount++;
 				document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
 			}
diff --git a/src/web/app/mobile/tags/page/messaging-room.tag b/src/web/app/mobile/tags/page/messaging-room.tag
index 67f46e4b1..262ece07a 100644
--- a/src/web/app/mobile/tags/page/messaging-room.tag
+++ b/src/web/app/mobile/tags/page/messaging-room.tag
@@ -14,7 +14,7 @@
 		this.fetching = true;
 
 		this.on('mount', () => {
-			this.api('users/show', {
+			this.$root.$data.os.api('users/show', {
 				username: this.opts.username
 			}).then(user => {
 				this.update({
diff --git a/src/web/app/mobile/tags/page/notifications.tag b/src/web/app/mobile/tags/page/notifications.tag
index eda5a1932..169ff029b 100644
--- a/src/web/app/mobile/tags/page/notifications.tag
+++ b/src/web/app/mobile/tags/page/notifications.tag
@@ -33,7 +33,7 @@
 
 			if (!ok) return;
 
-			this.api('notifications/mark_as_read_all');
+			this.$root.$data.os.api('notifications/mark_as_read_all');
 		};
 	</script>
 </mk-notifications-page>
diff --git a/src/web/app/mobile/tags/page/post.tag b/src/web/app/mobile/tags/page/post.tag
index 5e8cd2448..ed7cb5254 100644
--- a/src/web/app/mobile/tags/page/post.tag
+++ b/src/web/app/mobile/tags/page/post.tag
@@ -60,7 +60,7 @@
 
 			Progress.start();
 
-			this.api('posts/show', {
+			this.$root.$data.os.api('posts/show', {
 				post_id: this.opts.post
 			}).then(post => {
 
diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
index cafe65f27..6f7ef3ac3 100644
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ b/src/web/app/mobile/tags/page/settings/profile.tag
@@ -182,7 +182,7 @@
 					avatarSaving: true
 				});
 
-				this.api('i/update', {
+				this.$root.$data.os.api('i/update', {
 					avatar_id: file.id
 				}).then(() => {
 					this.update({
@@ -203,7 +203,7 @@
 					bannerSaving: true
 				});
 
-				this.api('i/update', {
+				this.$root.$data.os.api('i/update', {
 					banner_id: file.id
 				}).then(() => {
 					this.update({
@@ -230,7 +230,7 @@
 				saving: true
 			});
 
-			this.api('i/update', {
+			this.$root.$data.os.api('i/update', {
 				name: this.$refs.name.value,
 				location: this.$refs.location.value || null,
 				description: this.$refs.description.value || null,
diff --git a/src/web/app/mobile/tags/page/user-followers.tag b/src/web/app/mobile/tags/page/user-followers.tag
index 1123fd422..a65809484 100644
--- a/src/web/app/mobile/tags/page/user-followers.tag
+++ b/src/web/app/mobile/tags/page/user-followers.tag
@@ -18,7 +18,7 @@
 		this.on('mount', () => {
 			Progress.start();
 
-			this.api('users/show', {
+			this.$root.$data.os.api('users/show', {
 				username: this.opts.user
 			}).then(user => {
 				this.update({
diff --git a/src/web/app/mobile/tags/page/user-following.tag b/src/web/app/mobile/tags/page/user-following.tag
index b1c22cae1..8fe0f5fce 100644
--- a/src/web/app/mobile/tags/page/user-following.tag
+++ b/src/web/app/mobile/tags/page/user-following.tag
@@ -18,7 +18,7 @@
 		this.on('mount', () => {
 			Progress.start();
 
-			this.api('users/show', {
+			this.$root.$data.os.api('users/show', {
 				username: this.opts.user
 			}).then(user => {
 				this.update({
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index d812aba42..4b8566f96 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -291,7 +291,7 @@
 
 			// Get replies
 			if (!this.compact) {
-				this.api('posts/replies', {
+				this.$root.$data.os.api('posts/replies', {
 					post_id: this.p.id,
 					limit: 8
 				}).then(replies => {
@@ -311,7 +311,7 @@
 		this.repost = () => {
 			const text = window.prompt(`「${this.summary}」をRepost`);
 			if (text == null) return;
-			this.api('posts/create', {
+			this.$root.$data.os.api('posts/create', {
 				repost_id: this.p.id,
 				text: text == '' ? undefined : text
 			});
@@ -337,7 +337,7 @@
 			this.contextFetching = true;
 
 			// Fetch context
-			this.api('posts/context', {
+			this.$root.$data.os.api('posts/context', {
 				post_id: this.p.reply_id
 			}).then(context => {
 				this.update({
diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag
index c650fbce5..7b4d73f2d 100644
--- a/src/web/app/mobile/tags/search-posts.tag
+++ b/src/web/app/mobile/tags/search-posts.tag
@@ -25,7 +25,7 @@
 		this.query = this.opts.query;
 
 		this.init = new Promise((res, rej) => {
-			this.api('posts/search', parse(this.query)).then(posts => {
+			this.$root.$data.os.api('posts/search', parse(this.query)).then(posts => {
 				res(posts);
 				this.$emit('loaded');
 			});
@@ -33,7 +33,7 @@
 
 		this.more = () => {
 			this.offset += this.limit;
-			return this.api('posts/search', Object.assign({}, parse(this.query), {
+			return this.$root.$data.os.api('posts/search', Object.assign({}, parse(this.query), {
 				limit: this.limit,
 				offset: this.offset
 			}));
diff --git a/src/web/app/mobile/tags/user-followers.tag b/src/web/app/mobile/tags/user-followers.tag
index b9101e212..f3f70b2a6 100644
--- a/src/web/app/mobile/tags/user-followers.tag
+++ b/src/web/app/mobile/tags/user-followers.tag
@@ -11,7 +11,7 @@
 		this.user = this.opts.user;
 
 		this.fetch = (iknow, limit, cursor, cb) => {
-			this.api('users/followers', {
+			this.$root.$data.os.api('users/followers', {
 				user_id: this.user.id,
 				iknow: iknow,
 				limit: limit,
diff --git a/src/web/app/mobile/tags/user-following.tag b/src/web/app/mobile/tags/user-following.tag
index 5cfe60fec..b76757143 100644
--- a/src/web/app/mobile/tags/user-following.tag
+++ b/src/web/app/mobile/tags/user-following.tag
@@ -11,7 +11,7 @@
 		this.user = this.opts.user;
 
 		this.fetch = (iknow, limit, cursor, cb) => {
-			this.api('users/following', {
+			this.$root.$data.os.api('users/following', {
 				user_id: this.user.id,
 				iknow: iknow,
 				limit: limit,
diff --git a/src/web/app/mobile/tags/user-timeline.tag b/src/web/app/mobile/tags/user-timeline.tag
index b9f5dfbd5..546558155 100644
--- a/src/web/app/mobile/tags/user-timeline.tag
+++ b/src/web/app/mobile/tags/user-timeline.tag
@@ -13,7 +13,7 @@
 		this.withMedia = this.opts.withMedia;
 
 		this.init = new Promise((res, rej) => {
-			this.api('users/posts', {
+			this.$root.$data.os.api('users/posts', {
 				user_id: this.user.id,
 				with_media: this.withMedia
 			}).then(posts => {
@@ -23,7 +23,7 @@
 		});
 
 		this.more = () => {
-			return this.api('users/posts', {
+			return this.$root.$data.os.api('users/posts', {
 				user_id: this.user.id,
 				with_media: this.withMedia,
 				until_id: this.$refs.timeline.tail().id
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
index 87e63471e..b9bb4e17a 100644
--- a/src/web/app/mobile/tags/user.tag
+++ b/src/web/app/mobile/tags/user.tag
@@ -196,7 +196,7 @@
 		this.fetching = true;
 
 		this.on('mount', () => {
-			this.api('users/show', {
+			this.$root.$data.os.api('users/show', {
 				username: this.username
 			}).then(user => {
 				this.fetching = false;
@@ -348,7 +348,7 @@
 		this.initializing = true;
 
 		this.on('mount', () => {
-			this.api('users/posts', {
+			this.$root.$data.os.api('users/posts', {
 				user_id: this.user.id
 			}).then(posts => {
 				this.update({
@@ -485,7 +485,7 @@
 		this.user = this.opts.user;
 
 		this.on('mount', () => {
-			this.api('users/posts', {
+			this.$root.$data.os.api('users/posts', {
 				user_id: this.user.id,
 				with_media: true,
 				limit: 6
@@ -540,7 +540,7 @@
 		this.user = this.opts.user;
 
 		this.on('mount', () => {
-			this.api('aggregation/users/activity', {
+			this.$root.$data.os.api('aggregation/users/activity', {
 				user_id: this.user.id,
 				limit: 30
 			}).then(data => {
@@ -665,7 +665,7 @@
 		this.initializing = true;
 
 		this.on('mount', () => {
-			this.api('users/get_frequently_replied_users', {
+			this.$root.$data.os.api('users/get_frequently_replied_users', {
 				user_id: this.user.id
 			}).then(x => {
 				this.update({
@@ -720,7 +720,7 @@
 		this.initializing = true;
 
 		this.on('mount', () => {
-			this.api('users/followers', {
+			this.$root.$data.os.api('users/followers', {
 				user_id: this.user.id,
 				iknow: true,
 				limit: 30
diff --git a/src/web/app/mobile/views/components/follow-button.vue b/src/web/app/mobile/views/components/follow-button.vue
index 455be388c..047005cc9 100644
--- a/src/web/app/mobile/views/components/follow-button.vue
+++ b/src/web/app/mobile/views/components/follow-button.vue
@@ -56,7 +56,7 @@ export default Vue.extend({
 		onClick() {
 			this.wait = true;
 			if (this.user.is_following) {
-				this.api('following/delete', {
+				this.$root.$data.os.api('following/delete', {
 					user_id: this.user.id
 				}).then(() => {
 					this.user.is_following = false;
@@ -66,7 +66,7 @@ export default Vue.extend({
 					this.wait = false;
 				});
 			} else {
-				this.api('following/create', {
+				this.$root.$data.os.api('following/create', {
 					user_id: this.user.id
 				}).then(() => {
 					this.user.is_following = true;
diff --git a/src/web/app/stats/tags/index.tag b/src/web/app/stats/tags/index.tag
index 3b2b10b0a..4b167ccbc 100644
--- a/src/web/app/stats/tags/index.tag
+++ b/src/web/app/stats/tags/index.tag
@@ -46,7 +46,7 @@
 		this.initializing = true;
 
 		this.on('mount', () => {
-			this.api('stats').then(stats => {
+			this.$root.$data.os.api('stats').then(stats => {
 				this.update({
 					initializing: false,
 					stats
@@ -70,7 +70,7 @@
 		this.stats = this.opts.stats;
 
 		this.on('mount', () => {
-			this.api('aggregation/posts', {
+			this.$root.$data.os.api('aggregation/posts', {
 				limit: 365
 			}).then(data => {
 				this.update({
@@ -96,7 +96,7 @@
 		this.stats = this.opts.stats;
 
 		this.on('mount', () => {
-			this.api('aggregation/users', {
+			this.$root.$data.os.api('aggregation/users', {
 				limit: 365
 			}).then(data => {
 				this.update({
diff --git a/src/web/app/status/tags/index.tag b/src/web/app/status/tags/index.tag
index e06258c49..899467097 100644
--- a/src/web/app/status/tags/index.tag
+++ b/src/web/app/status/tags/index.tag
@@ -59,7 +59,7 @@
 		this.connection = new Connection();
 
 		this.on('mount', () => {
-			this.api('meta').then(meta => {
+			this.$root.$data.os.api('meta').then(meta => {
 				this.update({
 					initializing: false,
 					meta

From 43d9d81b538558312ddf447b071d0f871e3f5920 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 17:50:19 +0900
Subject: [PATCH 130/286] wip

---
 src/web/app/desktop/views/pages/home.vue      |  6 +-
 src/web/app/mobile/tags/page/home.tag         | 62 -------------------
 .../app/mobile/views/components/ui-header.vue | 11 ++--
 src/web/app/mobile/views/components/ui.vue    |  5 +-
 src/web/app/mobile/views/pages/home.vue       | 60 ++++++++++++++++++
 5 files changed, 72 insertions(+), 72 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/page/home.tag
 create mode 100644 src/web/app/mobile/views/pages/home.vue

diff --git a/src/web/app/desktop/views/pages/home.vue b/src/web/app/desktop/views/pages/home.vue
index ff20291d5..2dd7f47a4 100644
--- a/src/web/app/desktop/views/pages/home.vue
+++ b/src/web/app/desktop/views/pages/home.vue
@@ -1,7 +1,7 @@
 <template>
-	<mk-ui>
-		<mk-home ref="home" :mode="mode"/>
-	</mk-ui>
+<mk-ui>
+	<mk-home :mode="mode"/>
+</mk-ui>
 </template>
 
 <script lang="ts">
diff --git a/src/web/app/mobile/tags/page/home.tag b/src/web/app/mobile/tags/page/home.tag
deleted file mode 100644
index 10af292f3..000000000
--- a/src/web/app/mobile/tags/page/home.tag
+++ /dev/null
@@ -1,62 +0,0 @@
-<mk-home-page>
-	<mk-ui ref="ui">
-		<mk-home ref="home"/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import ui from '../../scripts/ui-event';
-		import Progress from '../../../common/scripts/loading';
-		import getPostSummary from '../../../../../common/get-post-summary.ts';
-		import openPostForm from '../../scripts/open-post-form';
-
-		this.mixin('i');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.unreadCount = 0;
-
-		this.on('mount', () => {
-			document.title = 'Misskey'
-			ui.trigger('title', '%fa:home%%i18n:mobile.tags.mk-home.home%');
-			document.documentElement.style.background = '#313a42';
-
-			ui.trigger('func', () => {
-				openPostForm();
-			}, '%fa:pencil-alt%');
-
-			Progress.start();
-
-			this.connection.on('post', this.onStreamPost);
-			document.addEventListener('visibilitychange', this.onVisibilitychange, false);
-
-			this.$refs.ui.refs.home.on('loaded', () => {
-				Progress.done();
-			});
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('post', this.onStreamPost);
-			this.stream.dispose(this.connectionId);
-			document.removeEventListener('visibilitychange', this.onVisibilitychange);
-		});
-
-		this.onStreamPost = post => {
-			if (document.hidden && post.user_id !== this.$root.$data.os.i.id) {
-				this.unreadCount++;
-				document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
-			}
-		};
-
-		this.onVisibilitychange = () => {
-			if (!document.hidden) {
-				this.unreadCount = 0;
-				document.title = 'Misskey';
-			}
-		};
-	</script>
-</mk-home-page>
diff --git a/src/web/app/mobile/views/components/ui-header.vue b/src/web/app/mobile/views/components/ui-header.vue
index 176751a66..3bb1054c8 100644
--- a/src/web/app/mobile/views/components/ui-header.vue
+++ b/src/web/app/mobile/views/components/ui-header.vue
@@ -6,8 +6,10 @@
 		<div class="content">
 			<button class="nav" @click="parent.toggleDrawer">%fa:bars%</button>
 			<template v-if="hasUnreadNotifications || hasUnreadMessagingMessages">%fa:circle%</template>
-			<h1 ref="title">Misskey</h1>
-			<button v-if="func" @click="func"><mk-raw content={ funcIcon }/></button>
+			<h1>
+				<slot>Misskey</slot>
+			</h1>
+			<button v-if="func" @click="func" v-html="funcIcon"></button>
 		</div>
 	</div>
 </div>
@@ -17,6 +19,7 @@
 import Vue from 'vue';
 
 export default Vue.extend({
+	props: ['func', 'funcIcon'],
 	data() {
 		return {
 			func: null,
@@ -62,10 +65,6 @@ export default Vue.extend({
 		}
 	},
 	methods: {
-		setFunc(fn, icon) {
-			this.func = fn;
-			this.funcIcon = icon;
-		},
 		onReadAllNotifications() {
 			this.hasUnreadNotifications = false;
 		},
diff --git a/src/web/app/mobile/views/components/ui.vue b/src/web/app/mobile/views/components/ui.vue
index aa5e2457c..52443430a 100644
--- a/src/web/app/mobile/views/components/ui.vue
+++ b/src/web/app/mobile/views/components/ui.vue
@@ -1,6 +1,8 @@
 <template>
 <div class="mk-ui">
-	<mk-ui-header/>
+	<mk-ui-header :func="func" :func-icon="funcIcon">
+		<slot name="header"></slot>
+	</mk-ui-header>
 	<mk-ui-nav :is-open="isDrawerOpening"/>
 	<div class="content">
 		<slot></slot>
@@ -12,6 +14,7 @@
 <script lang="ts">
 import Vue from 'vue';
 export default Vue.extend({
+	props: ['title', 'func', 'funcIcon'],
 	data() {
 		return {
 			isDrawerOpening: false,
diff --git a/src/web/app/mobile/views/pages/home.vue b/src/web/app/mobile/views/pages/home.vue
new file mode 100644
index 000000000..3b069c614
--- /dev/null
+++ b/src/web/app/mobile/views/pages/home.vue
@@ -0,0 +1,60 @@
+<template>
+<mk-ui :func="fn" func-icon="%fa:pencil-alt%">
+	<span slot="header">%fa:home%%i18n:mobile.tags.mk-home.home%</span>
+	<mk-home @loaded="onHomeLoaded"/>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+import getPostSummary from '../../../../../common/get-post-summary';
+import openPostForm from '../../scripts/open-post-form';
+
+export default Vue.extend({
+	data() {
+		return {
+			connection: null,
+			connectionId: null,
+			unreadCount: 0
+		};
+	},
+	mounted() {
+		document.title = 'Misskey';
+		document.documentElement.style.background = '#313a42';
+
+		this.connection = this.$root.$data.os.stream.getConnection();
+		this.connectionId = this.$root.$data.os.stream.use();
+
+		this.connection.on('post', this.onStreamPost);
+		document.addEventListener('visibilitychange', this.onVisibilitychange, false);
+
+		Progress.start();
+	},
+	beforeDestroy() {
+		this.connection.off('post', this.onStreamPost);
+		this.$root.$data.os.stream.dispose(this.connectionId);
+		document.removeEventListener('visibilitychange', this.onVisibilitychange);
+	},
+	methods: {
+		fn() {
+			openPostForm();
+		},
+		onHomeLoaded() {
+			Progress.done();
+		},
+		onStreamPost(post) {
+			if (document.hidden && post.user_id !== this.$root.$data.os.i.id) {
+				this.unreadCount++;
+				document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
+			}
+		},
+		onVisibilitychange() {
+			if (!document.hidden) {
+				this.unreadCount = 0;
+				document.title = 'Misskey';
+			}
+		}
+	}
+});
+</script>

From 4980b86d640ca795442255e4746c01bb38e0b5f8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 18:33:34 +0900
Subject: [PATCH 131/286] wip

---
 src/web/app/auth/tags/index.tag               |   6 +-
 src/web/app/ch/tags/channel.tag               |   8 +-
 src/web/app/ch/tags/header.tag                |   4 +-
 src/web/app/desktop/-tags/user-preview.tag    |   2 +-
 src/web/app/desktop/-tags/users-list.tag      |   2 +-
 src/web/app/mobile/tags/home.tag              |  23 -
 src/web/app/mobile/tags/user.tag              | 735 ------------------
 src/web/app/mobile/tags/users-list.tag        |   2 +-
 src/web/app/mobile/views/components/home.vue  |  29 +
 .../app/mobile/views/components/post-card.vue |  85 ++
 .../app/mobile/views/components/ui-nav.vue    |   2 +-
 src/web/app/mobile/views/pages/user.vue       | 226 ++++++
 .../views/pages/user/followers-you-know.vue   |  62 ++
 .../mobile/views/pages/user/home-activity.vue |  62 ++
 .../mobile/views/pages/user/home-friends.vue  |  54 ++
 .../mobile/views/pages/user/home-photos.vue   |  78 ++
 .../mobile/views/pages/user/home-posts.vue    |  57 ++
 src/web/app/mobile/views/pages/user/home.vue  |  95 +++
 18 files changed, 761 insertions(+), 771 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/home.tag
 delete mode 100644 src/web/app/mobile/tags/user.tag
 create mode 100644 src/web/app/mobile/views/components/home.vue
 create mode 100644 src/web/app/mobile/views/components/post-card.vue
 create mode 100644 src/web/app/mobile/views/pages/user.vue
 create mode 100644 src/web/app/mobile/views/pages/user/followers-you-know.vue
 create mode 100644 src/web/app/mobile/views/pages/user/home-activity.vue
 create mode 100644 src/web/app/mobile/views/pages/user/home-friends.vue
 create mode 100644 src/web/app/mobile/views/pages/user/home-photos.vue
 create mode 100644 src/web/app/mobile/views/pages/user/home-posts.vue
 create mode 100644 src/web/app/mobile/views/pages/user/home.vue

diff --git a/src/web/app/auth/tags/index.tag b/src/web/app/auth/tags/index.tag
index 3a24c2d6b..56fbbb7da 100644
--- a/src/web/app/auth/tags/index.tag
+++ b/src/web/app/auth/tags/index.tag
@@ -1,5 +1,5 @@
 <mk-index>
-	<main v-if="SIGNIN">
+	<main v-if="$root.$data.os.isSignedIn">
 		<p class="fetching" v-if="fetching">読み込み中<mk-ellipsis/></p>
 		<mk-form ref="form" v-if="state == 'waiting'" session={ session }/>
 		<div class="denied" v-if="state == 'denied'">
@@ -15,7 +15,7 @@
 			<p>セッションが存在しません。</p>
 		</div>
 	</main>
-	<main class="signin" v-if="!SIGNIN">
+	<main class="signin" v-if="!$root.$data.os.isSignedIn">
 		<h1>サインインしてください</h1>
 		<mk-signin/>
 	</main>
@@ -93,7 +93,7 @@
 		this.token = window.location.href.split('/').pop();
 
 		this.on('mount', () => {
-			if (!this.SIGNIN) return;
+			if (!this.$root.$data.os.isSignedIn) return;
 
 			// Fetch session
 			this.$root.$data.os.api('auth/session/show', {
diff --git a/src/web/app/ch/tags/channel.tag b/src/web/app/ch/tags/channel.tag
index d95de9737..b5c6ce1e6 100644
--- a/src/web/app/ch/tags/channel.tag
+++ b/src/web/app/ch/tags/channel.tag
@@ -4,7 +4,7 @@
 	<main v-if="!fetching">
 		<h1>{ channel.title }</h1>
 
-		<div v-if="SIGNIN">
+		<div v-if="$root.$data.os.isSignedIn">
 			<p v-if="channel.is_watching">このチャンネルをウォッチしています <a @click="unwatch">ウォッチ解除</a></p>
 			<p v-if="!channel.is_watching"><a @click="watch">このチャンネルをウォッチする</a></p>
 		</div>
@@ -24,8 +24,8 @@
 			</div>
 		</div>
 		<hr>
-		<mk-channel-form v-if="SIGNIN" channel={ channel } ref="form"/>
-		<div v-if="!SIGNIN">
+		<mk-channel-form v-if="$root.$data.os.isSignedIn" channel={ channel } ref="form"/>
+		<div v-if="!$root.$data.os.isSignedIn">
 			<p>参加するには<a href={ _URL_ }>ログインまたは新規登録</a>してください</p>
 		</div>
 		<hr>
@@ -125,7 +125,7 @@
 			this.posts.unshift(post);
 			this.update();
 
-			if (document.hidden && this.SIGNIN && post.user_id !== this.$root.$data.os.i.id) {
+			if (document.hidden && this.$root.$data.os.isSignedIn && post.user_id !== this.$root.$data.os.i.id) {
 				this.unreadCount++;
 				document.title = `(${this.unreadCount}) ${this.channel.title} | Misskey`;
 			}
diff --git a/src/web/app/ch/tags/header.tag b/src/web/app/ch/tags/header.tag
index 47a1e3e76..747bec357 100644
--- a/src/web/app/ch/tags/header.tag
+++ b/src/web/app/ch/tags/header.tag
@@ -3,8 +3,8 @@
 		<a href={ _CH_URL_ }>Index</a> | <a href={ _URL_ }>Misskey</a>
 	</div>
 	<div>
-		<a v-if="!SIGNIN" href={ _URL_ }>ログイン(新規登録)</a>
-		<a v-if="SIGNIN" href={ _URL_ + '/' + I.username }>{ I.username }</a>
+		<a v-if="!$root.$data.os.isSignedIn" href={ _URL_ }>ログイン(新規登録)</a>
+		<a v-if="$root.$data.os.isSignedIn" href={ _URL_ + '/' + I.username }>{ I.username }</a>
 	</div>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/desktop/-tags/user-preview.tag b/src/web/app/desktop/-tags/user-preview.tag
index 3a65fb79b..8503e9aeb 100644
--- a/src/web/app/desktop/-tags/user-preview.tag
+++ b/src/web/app/desktop/-tags/user-preview.tag
@@ -17,7 +17,7 @@
 				<p>フォロワー</p><a>{ user.followers_count }</a>
 			</div>
 		</div>
-		<mk-follow-button v-if="SIGNIN && user.id != I.id" user={ userPromise }/>
+		<mk-follow-button v-if="$root.$data.os.isSignedIn && user.id != I.id" user={ userPromise }/>
 	</template>
 	<style lang="stylus" scoped>
 		:scope
diff --git a/src/web/app/desktop/-tags/users-list.tag b/src/web/app/desktop/-tags/users-list.tag
index bf002ae55..03c527109 100644
--- a/src/web/app/desktop/-tags/users-list.tag
+++ b/src/web/app/desktop/-tags/users-list.tag
@@ -2,7 +2,7 @@
 	<nav>
 		<div>
 			<span data-is-active={ mode == 'all' } @click="setMode.bind(this, 'all')">すべて<span>{ opts.count }</span></span>
-			<span v-if="SIGNIN && opts.youKnowCount" data-is-active={ mode == 'iknow' } @click="setMode.bind(this, 'iknow')">知り合い<span>{ opts.youKnowCount }</span></span>
+			<span v-if="$root.$data.os.isSignedIn && opts.youKnowCount" data-is-active={ mode == 'iknow' } @click="setMode.bind(this, 'iknow')">知り合い<span>{ opts.youKnowCount }</span></span>
 		</div>
 	</nav>
 	<div class="users" v-if="!fetching && users.length != 0">
diff --git a/src/web/app/mobile/tags/home.tag b/src/web/app/mobile/tags/home.tag
deleted file mode 100644
index 038322b63..000000000
--- a/src/web/app/mobile/tags/home.tag
+++ /dev/null
@@ -1,23 +0,0 @@
-<mk-home>
-	<mk-home-timeline ref="tl"/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> mk-home-timeline
-				max-width 600px
-				margin 0 auto
-				padding 8px
-
-			@media (min-width 500px)
-				padding 16px
-
-	</style>
-	<script lang="typescript">
-		this.on('mount', () => {
-			this.$refs.tl.on('loaded', () => {
-				this.$emit('loaded');
-			});
-		});
-	</script>
-</mk-home>
diff --git a/src/web/app/mobile/tags/user.tag b/src/web/app/mobile/tags/user.tag
deleted file mode 100644
index b9bb4e17a..000000000
--- a/src/web/app/mobile/tags/user.tag
+++ /dev/null
@@ -1,735 +0,0 @@
-<mk-user>
-	<div class="user" v-if="!fetching">
-		<header>
-			<div class="banner" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=1024)' : '' }></div>
-			<div class="body">
-				<div class="top">
-					<a class="avatar">
-						<img src={ user.avatar_url + '?thumbnail&size=200' } alt="avatar"/>
-					</a>
-					<mk-follow-button v-if="SIGNIN && I.id != user.id" user={ user }/>
-				</div>
-				<div class="title">
-					<h1>{ user.name }</h1>
-					<span class="username">@{ user.username }</span>
-					<span class="followed" v-if="user.is_followed">%i18n:mobile.tags.mk-user.follows-you%</span>
-				</div>
-				<div class="description">{ user.description }</div>
-				<div class="info">
-					<p class="location" v-if="user.profile.location">
-						%fa:map-marker%{ user.profile.location }
-					</p>
-					<p class="birthday" v-if="user.profile.birthday">
-						%fa:birthday-cake%{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' } ({ age(user.profile.birthday) }歳)
-					</p>
-				</div>
-				<div class="status">
-				  <a>
-				    <b>{ user.posts_count }</b>
-						<i>%i18n:mobile.tags.mk-user.posts%</i>
-					</a>
-					<a href="{ user.username }/following">
-						<b>{ user.following_count }</b>
-						<i>%i18n:mobile.tags.mk-user.following%</i>
-					</a>
-					<a href="{ user.username }/followers">
-						<b>{ user.followers_count }</b>
-						<i>%i18n:mobile.tags.mk-user.followers%</i>
-					</a>
-				</div>
-			</div>
-			<nav>
-				<a data-is-active={ page == 'overview' } @click="go.bind(null, 'overview')">%i18n:mobile.tags.mk-user.overview%</a>
-				<a data-is-active={ page == 'posts' } @click="go.bind(null, 'posts')">%i18n:mobile.tags.mk-user.timeline%</a>
-				<a data-is-active={ page == 'media' } @click="go.bind(null, 'media')">%i18n:mobile.tags.mk-user.media%</a>
-			</nav>
-		</header>
-		<div class="body">
-			<mk-user-overview v-if="page == 'overview'" user={ user }/>
-			<mk-user-timeline v-if="page == 'posts'" user={ user }/>
-			<mk-user-timeline v-if="page == 'media'" user={ user } with-media={ true }/>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> .user
-				> header
-					box-shadow 0 4px 4px rgba(0, 0, 0, 0.3)
-
-					> .banner
-						padding-bottom 33.3%
-						background-color #1b1b1b
-						background-size cover
-						background-position center
-
-					> .body
-						padding 12px
-						margin 0 auto
-						max-width 600px
-
-						> .top
-							&:after
-								content ''
-								display block
-								clear both
-
-							> .avatar
-								display block
-								float left
-								width 25%
-								height 40px
-
-								> img
-									display block
-									position absolute
-									left -2px
-									bottom -2px
-									width 100%
-									border 2px solid #313a42
-									border-radius 6px
-
-									@media (min-width 500px)
-										left -4px
-										bottom -4px
-										border 4px solid #313a42
-										border-radius 12px
-
-							> mk-follow-button
-								float right
-								height 40px
-
-						> .title
-							margin 8px 0
-
-							> h1
-								margin 0
-								line-height 22px
-								font-size 20px
-								color #fff
-
-							> .username
-								display inline-block
-								line-height 20px
-								font-size 16px
-								font-weight bold
-								color #657786
-
-							> .followed
-								margin-left 8px
-								padding 2px 4px
-								font-size 12px
-								color #657786
-								background #f8f8f8
-								border-radius 4px
-
-						> .description
-							margin 8px 0
-							color #fff
-
-						> .info
-							margin 8px 0
-
-							> p
-								display inline
-								margin 0 16px 0 0
-								color #a9b9c1
-
-								> i
-									margin-right 4px
-
-						> .status
-							> a
-								color #657786
-
-								&:not(:last-child)
-									margin-right 16px
-
-								> b
-									margin-right 4px
-									font-size 16px
-									color #fff
-
-								> i
-									font-size 14px
-
-						> mk-activity-table
-							margin 12px 0 0 0
-
-					> nav
-						display flex
-						justify-content center
-						margin 0 auto
-						max-width 600px
-
-						> a
-							display block
-							flex 1 1
-							text-align center
-							line-height 52px
-							font-size 14px
-							text-decoration none
-							color #657786
-							border-bottom solid 2px transparent
-
-							&[data-is-active]
-								font-weight bold
-								color $theme-color
-								border-color $theme-color
-
-				> .body
-					padding 8px
-
-					@media (min-width 500px)
-						padding 16px
-
-	</style>
-	<script lang="typescript">
-		this.age = require('s-age');
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.username = this.opts.user;
-		this.page = this.opts.page ? this.opts.page : 'overview';
-		this.fetching = true;
-
-		this.on('mount', () => {
-			this.$root.$data.os.api('users/show', {
-				username: this.username
-			}).then(user => {
-				this.fetching = false;
-				this.user = user;
-				this.$emit('loaded', user);
-				this.update();
-			});
-		});
-
-		this.go = page => {
-			this.update({
-				page: page
-			});
-		};
-	</script>
-</mk-user>
-
-<mk-user-overview>
-	<mk-post-detail v-if="user.pinned_post" post={ user.pinned_post } compact={ true }/>
-	<section class="recent-posts">
-		<h2>%fa:R comments%%i18n:mobile.tags.mk-user-overview.recent-posts%</h2>
-		<div>
-			<mk-user-overview-posts user={ user }/>
-		</div>
-	</section>
-	<section class="images">
-		<h2>%fa:image%%i18n:mobile.tags.mk-user-overview.images%</h2>
-		<div>
-			<mk-user-overview-photos user={ user }/>
-		</div>
-	</section>
-	<section class="activity">
-		<h2>%fa:chart-bar%%i18n:mobile.tags.mk-user-overview.activity%</h2>
-		<div>
-			<mk-user-overview-activity-chart user={ user }/>
-		</div>
-	</section>
-	<section class="keywords">
-		<h2>%fa:R comment%%i18n:mobile.tags.mk-user-overview.keywords%</h2>
-		<div>
-			<mk-user-overview-keywords user={ user }/>
-		</div>
-	</section>
-	<section class="domains">
-		<h2>%fa:globe%%i18n:mobile.tags.mk-user-overview.domains%</h2>
-		<div>
-			<mk-user-overview-domains user={ user }/>
-		</div>
-	</section>
-	<section class="frequently-replied-users">
-		<h2>%fa:users%%i18n:mobile.tags.mk-user-overview.frequently-replied-users%</h2>
-		<div>
-			<mk-user-overview-frequently-replied-users user={ user }/>
-		</div>
-	</section>
-	<section class="followers-you-know" v-if="SIGNIN && I.id !== user.id">
-		<h2>%fa:users%%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2>
-		<div>
-			<mk-user-overview-followers-you-know user={ user }/>
-		</div>
-	</section>
-	<p>%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time time={ user.last_used_at }/></b></p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			max-width 600px
-			margin 0 auto
-
-			> mk-post-detail
-				margin 0 0 8px 0
-
-			> section
-				background #eee
-				border-radius 8px
-				box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
-
-				&:not(:last-child)
-					margin-bottom 8px
-
-				> h2
-					margin 0
-					padding 8px 10px
-					font-size 15px
-					font-weight normal
-					color #465258
-					background #fff
-					border-radius 8px 8px 0 0
-
-					> i
-						margin-right 6px
-
-			> .activity
-				> div
-					padding 8px
-
-			> p
-				display block
-				margin 16px
-				text-align center
-				color #cad2da
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-
-		this.user = this.opts.user;
-	</script>
-</mk-user-overview>
-
-<mk-user-overview-posts>
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-posts.loading%<mk-ellipsis/></p>
-	<div v-if="!initializing && posts.length > 0">
-		<template each={ posts }>
-			<mk-user-overview-posts-post-card post={ this }/>
-		</template>
-	</div>
-	<p class="empty" v-if="!initializing && posts.length == 0">%i18n:mobile.tags.mk-user-overview-posts.no-posts%</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> div
-				overflow-x scroll
-				-webkit-overflow-scrolling touch
-				white-space nowrap
-				padding 8px
-
-				> *
-					vertical-align top
-
-					&:not(:last-child)
-						margin-right 8px
-
-			> .initializing
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> i
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.user = this.opts.user;
-		this.initializing = true;
-
-		this.on('mount', () => {
-			this.$root.$data.os.api('users/posts', {
-				user_id: this.user.id
-			}).then(posts => {
-				this.update({
-					posts: posts,
-					initializing: false
-				});
-			});
-		});
-	</script>
-</mk-user-overview-posts>
-
-<mk-user-overview-posts-post-card>
-	<a href={ '/' + post.user.username + '/' + post.id }>
-		<header>
-			<img src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/><h3>{ post.user.name }</h3>
-		</header>
-		<div>
-			{ text }
-		</div>
-		<mk-time time={ post.created_at }/>
-	</a>
-	<style lang="stylus" scoped>
-		:scope
-			display inline-block
-			width 150px
-			//height 120px
-			font-size 12px
-			background #fff
-			border-radius 4px
-
-			> a
-				display block
-				color #2c3940
-
-				&:hover
-					text-decoration none
-
-				> header
-					> img
-						position absolute
-						top 8px
-						left 8px
-						width 28px
-						height 28px
-						border-radius 6px
-
-					> h3
-						display inline-block
-						overflow hidden
-						width calc(100% - 45px)
-						margin 8px 0 0 42px
-						line-height 28px
-						white-space nowrap
-						text-overflow ellipsis
-						font-size 12px
-
-				> div
-					padding 2px 8px 8px 8px
-					height 60px
-					overflow hidden
-					white-space normal
-
-					&:after
-						content ""
-						display block
-						position absolute
-						top 40px
-						left 0
-						width 100%
-						height 20px
-						background linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #fff 100%)
-
-				> mk-time
-					display inline-block
-					padding 8px
-					color #aaa
-
-	</style>
-	<script lang="typescript">
-		import summary from '../../../../common/get-post-summary.ts';
-
-		this.post = this.opts.post;
-		this.text = summary(this.post);
-	</script>
-</mk-user-overview-posts-post-card>
-
-<mk-user-overview-photos>
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-photos.loading%<mk-ellipsis/></p>
-	<div class="stream" v-if="!initializing && images.length > 0">
-		<template each={ image in images }>
-			<a class="img" style={ 'background-image: url(' + image.media.url + '?thumbnail&size=256)' } href={ '/' + image.post.user.username + '/' + image.post.id }></a>
-		</template>
-	</div>
-	<p class="empty" v-if="!initializing && images.length == 0">%i18n:mobile.tags.mk-user-overview-photos.no-photos%</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> .stream
-				display -webkit-flex
-				display -moz-flex
-				display -ms-flex
-				display flex
-				justify-content center
-				flex-wrap wrap
-				padding 8px
-
-				> .img
-					flex 1 1 33%
-					width 33%
-					height 80px
-					background-position center center
-					background-size cover
-					background-clip content-box
-					border solid 2px transparent
-					border-radius 4px
-
-			> .initializing
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> i
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.images = [];
-		this.initializing = true;
-		this.user = this.opts.user;
-
-		this.on('mount', () => {
-			this.$root.$data.os.api('users/posts', {
-				user_id: this.user.id,
-				with_media: true,
-				limit: 6
-			}).then(posts => {
-				this.initializing = false;
-				posts.forEach(post => {
-					post.media.forEach(media => {
-						if (this.images.length < 9) this.images.push({
-							post,
-							media
-						});
-					});
-				});
-				this.update();
-			});
-		});
-	</script>
-</mk-user-overview-photos>
-
-<mk-user-overview-activity-chart>
-	<svg v-if="data" ref="canvas" viewBox="0 0 30 1" preserveAspectRatio="none">
-		<g each={ d, i in data.reverse() }>
-			<rect width="0.8" riot-height={ d.postsH }
-				riot-x={ i + 0.1 } riot-y={ 1 - d.postsH - d.repliesH - d.repostsH }
-				fill="#41ddde"/>
-			<rect width="0.8" riot-height={ d.repliesH }
-				riot-x={ i + 0.1 } riot-y={ 1 - d.repliesH - d.repostsH }
-				fill="#f7796c"/>
-			<rect width="0.8" riot-height={ d.repostsH }
-				riot-x={ i + 0.1 } riot-y={ 1 - d.repostsH }
-				fill="#a1de41"/>
-			</g>
-	</svg>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			max-width 600px
-			margin 0 auto
-
-			> svg
-				display block
-				width 100%
-				height 80px
-
-				> rect
-					transform-origin center
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.user = this.opts.user;
-
-		this.on('mount', () => {
-			this.$root.$data.os.api('aggregation/users/activity', {
-				user_id: this.user.id,
-				limit: 30
-			}).then(data => {
-				data.forEach(d => d.total = d.posts + d.replies + d.reposts);
-				this.peak = Math.max.apply(null, data.map(d => d.total));
-				data.forEach(d => {
-					d.postsH = d.posts / this.peak;
-					d.repliesH = d.replies / this.peak;
-					d.repostsH = d.reposts / this.peak;
-				});
-				this.update({ data });
-			});
-		});
-	</script>
-</mk-user-overview-activity-chart>
-
-<mk-user-overview-keywords>
-	<div v-if="user.keywords != null && user.keywords.length > 1">
-		<template each={ keyword in user.keywords }>
-			<a>{ keyword }</a>
-		</template>
-	</div>
-	<p class="empty" v-if="user.keywords == null || user.keywords.length == 0">%i18n:mobile.tags.mk-user-overview-keywords.no-keywords%</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> div
-				padding 4px
-
-				> a
-					display inline-block
-					margin 4px
-					color #555
-
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> i
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.user = this.opts.user;
-	</script>
-</mk-user-overview-keywords>
-
-<mk-user-overview-domains>
-	<div v-if="user.domains != null && user.domains.length > 1">
-		<template each={ domain in user.domains }>
-			<a style="opacity: { 0.5 + (domain.weight / 2) }">{ domain.domain }</a>
-		</template>
-	</div>
-	<p class="empty" v-if="user.domains == null || user.domains.length == 0">%i18n:mobile.tags.mk-user-overview-domains.no-domains%</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> div
-				padding 4px
-
-				> a
-					display inline-block
-					margin 4px
-					color #555
-
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> i
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.user = this.opts.user;
-	</script>
-</mk-user-overview-domains>
-
-<mk-user-overview-frequently-replied-users>
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-frequently-replied-users.loading%<mk-ellipsis/></p>
-	<div v-if="!initializing && users.length > 0">
-		<template each={ users }>
-			<mk-user-card user={ this.user }/>
-		</template>
-	</div>
-	<p class="empty" v-if="!initializing && users.length == 0">%i18n:mobile.tags.mk-user-overview-frequently-replied-users.no-users%</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> div
-				overflow-x scroll
-				-webkit-overflow-scrolling touch
-				white-space nowrap
-				padding 8px
-
-				> mk-user-card
-					&:not(:last-child)
-						margin-right 8px
-
-			> .initializing
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> i
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.user = this.opts.user;
-		this.initializing = true;
-
-		this.on('mount', () => {
-			this.$root.$data.os.api('users/get_frequently_replied_users', {
-				user_id: this.user.id
-			}).then(x => {
-				this.update({
-					users: x,
-					initializing: false
-				});
-			});
-		});
-	</script>
-</mk-user-overview-frequently-replied-users>
-
-<mk-user-overview-followers-you-know>
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p>
-	<div v-if="!initializing && users.length > 0">
-		<template each={ user in users }>
-			<a href={ '/' + user.username }><img src={ user.avatar_url + '?thumbnail&size=64' } alt={ user.name }/></a>
-		</template>
-	</div>
-	<p class="empty" v-if="!initializing && users.length == 0">%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> div
-				padding 4px
-
-				> a
-					display inline-block
-					margin 4px
-
-					> img
-						width 48px
-						height 48px
-						vertical-align bottom
-						border-radius 100%
-
-			> .initializing
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> i
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.user = this.opts.user;
-		this.initializing = true;
-
-		this.on('mount', () => {
-			this.$root.$data.os.api('users/followers', {
-				user_id: this.user.id,
-				iknow: true,
-				limit: 30
-			}).then(x => {
-				this.update({
-					users: x.users,
-					initializing: false
-				});
-			});
-		});
-	</script>
-</mk-user-overview-followers-you-know>
diff --git a/src/web/app/mobile/tags/users-list.tag b/src/web/app/mobile/tags/users-list.tag
index 2bc0c6e93..84427a18e 100644
--- a/src/web/app/mobile/tags/users-list.tag
+++ b/src/web/app/mobile/tags/users-list.tag
@@ -1,7 +1,7 @@
 <mk-users-list>
 	<nav>
 		<span data-is-active={ mode == 'all' } @click="setMode.bind(this, 'all')">%i18n:mobile.tags.mk-users-list.all%<span>{ opts.count }</span></span>
-		<span v-if="SIGNIN && opts.youKnowCount" data-is-active={ mode == 'iknow' } @click="setMode.bind(this, 'iknow')">%i18n:mobile.tags.mk-users-list.known%<span>{ opts.youKnowCount }</span></span>
+		<span v-if="$root.$data.os.isSignedIn && opts.youKnowCount" data-is-active={ mode == 'iknow' } @click="setMode.bind(this, 'iknow')">%i18n:mobile.tags.mk-users-list.known%<span>{ opts.youKnowCount }</span></span>
 	</nav>
 	<div class="users" v-if="!fetching && users.length != 0">
 		<mk-user-preview each={ users } user={ this }/>
diff --git a/src/web/app/mobile/views/components/home.vue b/src/web/app/mobile/views/components/home.vue
new file mode 100644
index 000000000..3feab581d
--- /dev/null
+++ b/src/web/app/mobile/views/components/home.vue
@@ -0,0 +1,29 @@
+<template>
+<div class="mk-home">
+	<mk-timeline @loaded="onTlLoaded"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	methods: {
+		onTlLoaded() {
+			this.$emit('loaded');
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-home
+
+	> .mk-timeline
+		max-width 600px
+		margin 0 auto
+		padding 8px
+
+	@media (min-width 500px)
+		padding 16px
+
+</style>
diff --git a/src/web/app/mobile/views/components/post-card.vue b/src/web/app/mobile/views/components/post-card.vue
new file mode 100644
index 000000000..4dd6ceb28
--- /dev/null
+++ b/src/web/app/mobile/views/components/post-card.vue
@@ -0,0 +1,85 @@
+<template>
+<div class="mk-post-card">
+	<a :href="`/${post.user.username}/${post.id}`">
+		<header>
+			<img :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/><h3>{{ post.user.name }}</h3>
+		</header>
+		<div>
+			{{ text }}
+		</div>
+		<mk-time :time="post.created_at"/>
+	</a>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import summary from '../../../../../common/get-post-summary';
+
+export default Vue.extend({
+	props: ['post'],
+	computed: {
+		text(): string {
+			return summary(this.post);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-post-card
+	display inline-block
+	width 150px
+	//height 120px
+	font-size 12px
+	background #fff
+	border-radius 4px
+
+	> a
+		display block
+		color #2c3940
+
+		&:hover
+			text-decoration none
+
+		> header
+			> img
+				position absolute
+				top 8px
+				left 8px
+				width 28px
+				height 28px
+				border-radius 6px
+
+			> h3
+				display inline-block
+				overflow hidden
+				width calc(100% - 45px)
+				margin 8px 0 0 42px
+				line-height 28px
+				white-space nowrap
+				text-overflow ellipsis
+				font-size 12px
+
+		> div
+			padding 2px 8px 8px 8px
+			height 60px
+			overflow hidden
+			white-space normal
+
+			&:after
+				content ""
+				display block
+				position absolute
+				top 40px
+				left 0
+				width 100%
+				height 20px
+				background linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #fff 100%)
+
+		> mk-time
+			display inline-block
+			padding 8px
+			color #aaa
+
+</style>
diff --git a/src/web/app/mobile/views/components/ui-nav.vue b/src/web/app/mobile/views/components/ui-nav.vue
index 3765ce887..cab24787d 100644
--- a/src/web/app/mobile/views/components/ui-nav.vue
+++ b/src/web/app/mobile/views/components/ui-nav.vue
@@ -2,7 +2,7 @@
 <div class="mk-ui-nav" :style="{ display: isOpen ? 'block' : 'none' }">
 	<div class="backdrop" @click="parent.toggleDrawer"></div>
 	<div class="body">
-		<a class="me" v-if="SIGNIN" href={ '/' + I.username }>
+		<a class="me" v-if="$root.$data.os.isSignedIn" href={ '/' + I.username }>
 			<img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/>
 			<p class="name">{ I.name }</p>
 		</a>
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
new file mode 100644
index 000000000..d92f3bbe6
--- /dev/null
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -0,0 +1,226 @@
+<template>
+<mk-ui :func="fn" func-icon="%fa:pencil-alt%">
+	<span slot="header">%fa:user% {{user.name}}</span>
+	<div v-if="!fetching" :class="$style.user">
+		<header>
+			<div class="banner" :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=1024)` : ''"></div>
+			<div class="body">
+				<div class="top">
+					<a class="avatar">
+						<img :src="`${user.avatar_url}?thumbnail&size=200`" alt="avatar"/>
+					</a>
+					<mk-follow-button v-if="$root.$data.os.isSignedIn && $root.$data.os.i.id != user.id" :user="user"/>
+				</div>
+				<div class="title">
+					<h1>{{ user.name }}</h1>
+					<span class="username">@{{ user.username }}</span>
+					<span class="followed" v-if="user.is_followed">%i18n:mobile.tags.mk-user.follows-you%</span>
+				</div>
+				<div class="description">{{ user.description }}</div>
+				<div class="info">
+					<p class="location" v-if="user.profile.location">
+						%fa:map-marker%{{ user.profile.location }}
+					</p>
+					<p class="birthday" v-if="user.profile.birthday">
+						%fa:birthday-cake%{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)
+					</p>
+				</div>
+				<div class="status">
+				  <a>
+				    <b>{{ user.posts_count }}</b>
+						<i>%i18n:mobile.tags.mk-user.posts%</i>
+					</a>
+					<a :href="`${user.username}/following`">
+						<b>{{ user.following_count }}</b>
+						<i>%i18n:mobile.tags.mk-user.following%</i>
+					</a>
+					<a :href="`${user.username}/followers`">
+						<b>{{ user.followers_count }}</b>
+						<i>%i18n:mobile.tags.mk-user.followers%</i>
+					</a>
+				</div>
+			</div>
+			<nav>
+				<a :data-is-active=" page == 'home' " @click="page = 'home'">%i18n:mobile.tags.mk-user.overview%</a>
+				<a :data-is-active=" page == 'posts' " @click="page = 'posts'">%i18n:mobile.tags.mk-user.timeline%</a>
+				<a :data-is-active=" page == 'media' " @click="page = 'media'">%i18n:mobile.tags.mk-user.media%</a>
+			</nav>
+		</header>
+		<div class="body">
+			<mk-user-home v-if="page == 'home'" :user="user"/>
+			<mk-user-timeline v-if="page == 'posts'" :user="user"/>
+			<mk-user-timeline v-if="page == 'media'" :user="user" with-media/>
+		</div>
+	</div>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+const age = require('s-age');
+
+export default Vue.extend({
+	props: {
+		username: {
+			type: String,
+			required: true
+		},
+		page: {
+			default: 'home'
+		}
+	},
+	data() {
+		return {
+			fetching: true,
+			user: null
+		};
+	},
+	computed: {
+		age(): number {
+			return age(this.user.profile.birthday);
+		}
+	},
+	mounted() {
+		this.$root.$data.os.api('users/show', {
+			username: this.username
+		}).then(user => {
+			this.fetching = false;
+			this.user = user;
+			this.$emit('loaded', user);
+		});
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.user
+	> header
+		box-shadow 0 4px 4px rgba(0, 0, 0, 0.3)
+
+		> .banner
+			padding-bottom 33.3%
+			background-color #1b1b1b
+			background-size cover
+			background-position center
+
+		> .body
+			padding 12px
+			margin 0 auto
+			max-width 600px
+
+			> .top
+				&:after
+					content ''
+					display block
+					clear both
+
+				> .avatar
+					display block
+					float left
+					width 25%
+					height 40px
+
+					> img
+						display block
+						position absolute
+						left -2px
+						bottom -2px
+						width 100%
+						border 2px solid #313a42
+						border-radius 6px
+
+						@media (min-width 500px)
+							left -4px
+							bottom -4px
+							border 4px solid #313a42
+							border-radius 12px
+
+				> mk-follow-button
+					float right
+					height 40px
+
+			> .title
+				margin 8px 0
+
+				> h1
+					margin 0
+					line-height 22px
+					font-size 20px
+					color #fff
+
+				> .username
+					display inline-block
+					line-height 20px
+					font-size 16px
+					font-weight bold
+					color #657786
+
+				> .followed
+					margin-left 8px
+					padding 2px 4px
+					font-size 12px
+					color #657786
+					background #f8f8f8
+					border-radius 4px
+
+			> .description
+				margin 8px 0
+				color #fff
+
+			> .info
+				margin 8px 0
+
+				> p
+					display inline
+					margin 0 16px 0 0
+					color #a9b9c1
+
+					> i
+						margin-right 4px
+
+			> .status
+				> a
+					color #657786
+
+					&:not(:last-child)
+						margin-right 16px
+
+					> b
+						margin-right 4px
+						font-size 16px
+						color #fff
+
+					> i
+						font-size 14px
+
+			> mk-activity-table
+				margin 12px 0 0 0
+
+		> nav
+			display flex
+			justify-content center
+			margin 0 auto
+			max-width 600px
+
+			> a
+				display block
+				flex 1 1
+				text-align center
+				line-height 52px
+				font-size 14px
+				text-decoration none
+				color #657786
+				border-bottom solid 2px transparent
+
+				&[data-is-active]
+					font-weight bold
+					color $theme-color
+					border-color $theme-color
+
+	> .body
+		padding 8px
+
+		@media (min-width 500px)
+			padding 16px
+
+</style>
diff --git a/src/web/app/mobile/views/pages/user/followers-you-know.vue b/src/web/app/mobile/views/pages/user/followers-you-know.vue
new file mode 100644
index 000000000..a4358f5d9
--- /dev/null
+++ b/src/web/app/mobile/views/pages/user/followers-you-know.vue
@@ -0,0 +1,62 @@
+<template>
+<div class="mk-user-home-followers-you-know">
+	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p>
+	<div v-if="!fetching && users.length > 0">
+		<a v-for="user in users" :key="user.id" :href="`/${user.username}`">
+			<img :src="`${user.avatar_url}?thumbnail&size=64`" :alt="user.name"/>
+		</a>
+	</div>
+	<p class="empty" v-if="!fetching && users.length == 0">%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	data() {
+		return {
+			fetching: true,
+			users: []
+		};
+	},
+	mounted() {
+		this.$root.$data.os.api('users/followers', {
+			user_id: this.user.id,
+			iknow: true,
+			limit: 30
+		}).then(res => {
+			this.fetching = false;
+			this.users = res.users;
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-home-followers-you-know
+
+	> div
+		padding 4px
+
+		> a
+			display inline-block
+			margin 4px
+
+			> img
+				width 48px
+				height 48px
+				vertical-align bottom
+				border-radius 100%
+
+	> .initializing
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> i
+			margin-right 4px
+
+</style>
diff --git a/src/web/app/mobile/views/pages/user/home-activity.vue b/src/web/app/mobile/views/pages/user/home-activity.vue
new file mode 100644
index 000000000..00a2dafc1
--- /dev/null
+++ b/src/web/app/mobile/views/pages/user/home-activity.vue
@@ -0,0 +1,62 @@
+<template>
+<div class="mk-user-home-activity">
+	<svg v-if="data" ref="canvas" viewBox="0 0 30 1" preserveAspectRatio="none">
+		<g v-for="(d, i) in data.reverse()" :key="i">
+			<rect width="0.8" :height="d.postsH"
+				:x="i + 0.1" :y="1 - d.postsH - d.repliesH - d.repostsH"
+				fill="#41ddde"/>
+			<rect width="0.8" :height="d.repliesH"
+				:x="i + 0.1" :y="1 - d.repliesH - d.repostsH"
+				fill="#f7796c"/>
+			<rect width="0.8" :height="d.repostsH"
+				:x="i + 0.1" :y="1 - d.repostsH"
+				fill="#a1de41"/>
+			</g>
+	</svg>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	data() {
+		return {
+			fetching: true,
+			data: [],
+			peak: null
+		};
+	},
+	mounted() {
+		this.$root.$data.os.api('aggregation/users/activity', {
+			user_id: this.user.id,
+			limit: 30
+		}).then(data => {
+			data.forEach(d => d.total = d.posts + d.replies + d.reposts);
+			this.peak = Math.max.apply(null, data.map(d => d.total));
+			data.forEach(d => {
+				d.postsH = d.posts / this.peak;
+				d.repliesH = d.replies / this.peak;
+				d.repostsH = d.reposts / this.peak;
+			});
+			this.data = data;
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-home-activity
+	display block
+	max-width 600px
+	margin 0 auto
+
+	> svg
+		display block
+		width 100%
+		height 80px
+
+		> rect
+			transform-origin center
+
+</style>
diff --git a/src/web/app/mobile/views/pages/user/home-friends.vue b/src/web/app/mobile/views/pages/user/home-friends.vue
new file mode 100644
index 000000000..2a7e8b961
--- /dev/null
+++ b/src/web/app/mobile/views/pages/user/home-friends.vue
@@ -0,0 +1,54 @@
+<template>
+<div class="mk-user-home-friends">
+	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-frequently-replied-users.loading%<mk-ellipsis/></p>
+	<div v-if="!fetching && users.length > 0">
+		<mk-user-card v-for="user in users" :key="user.id" :user="user"/>
+	</div>
+	<p class="empty" v-if="!fetching && users.length == 0">%i18n:mobile.tags.mk-user-overview-frequently-replied-users.no-users%</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	data() {
+		return {
+			fetching: true,
+			users: []
+		};
+	},
+	mounted() {
+		this.$root.$data.os.api('users/get_frequently_replied_users', {
+			user_id: this.user.id
+		}).then(res => {
+			this.fetching = false;
+			this.users = res.map(x => x.user);
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-home-friends
+	> div
+		overflow-x scroll
+		-webkit-overflow-scrolling touch
+		white-space nowrap
+		padding 8px
+
+		> mk-user-card
+			&:not(:last-child)
+				margin-right 8px
+
+	> .initializing
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> i
+			margin-right 4px
+
+</style>
diff --git a/src/web/app/mobile/views/pages/user/home-photos.vue b/src/web/app/mobile/views/pages/user/home-photos.vue
new file mode 100644
index 000000000..fc2d0e139
--- /dev/null
+++ b/src/web/app/mobile/views/pages/user/home-photos.vue
@@ -0,0 +1,78 @@
+<template>
+<div class="mk-user-home-photos">
+	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-photos.loading%<mk-ellipsis/></p>
+	<div class="stream" v-if="!fetching && images.length > 0">
+		<a v-for="image in images" :key="image.id"
+			class="img"
+			:style="`background-image: url(${image.media.url}?thumbnail&size=256)`"
+			:href="`/${image.post.user.username}/${image.post.id}`"
+		></a>
+	</div>
+	<p class="empty" v-if="!fetching && images.length == 0">%i18n:mobile.tags.mk-user-overview-photos.no-photos%</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	data() {
+		return {
+			fetching: true,
+			images: []
+		};
+	},
+	mounted() {
+		this.$root.$data.os.api('users/posts', {
+			user_id: this.user.id,
+			with_media: true,
+			limit: 6
+		}).then(posts => {
+			this.fetching = false;
+			posts.forEach(post => {
+				post.media.forEach(media => {
+					if (this.images.length < 9) this.images.push({
+						post,
+						media
+					});
+				});
+			});
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-home-photos
+
+	> .stream
+		display -webkit-flex
+		display -moz-flex
+		display -ms-flex
+		display flex
+		justify-content center
+		flex-wrap wrap
+		padding 8px
+
+		> .img
+			flex 1 1 33%
+			width 33%
+			height 80px
+			background-position center center
+			background-size cover
+			background-clip content-box
+			border solid 2px transparent
+			border-radius 4px
+
+	> .initializing
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> i
+			margin-right 4px
+
+</style>
+
diff --git a/src/web/app/mobile/views/pages/user/home-posts.vue b/src/web/app/mobile/views/pages/user/home-posts.vue
new file mode 100644
index 000000000..b1451b088
--- /dev/null
+++ b/src/web/app/mobile/views/pages/user/home-posts.vue
@@ -0,0 +1,57 @@
+<template>
+<div class="mk-user-home-posts">
+	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-posts.loading%<mk-ellipsis/></p>
+	<div v-if="!initializing && posts.length > 0">
+		<mk-post-card v-for="post in posts" :key="post.id" :post="post"/>
+	</div>
+	<p class="empty" v-if="!initializing && posts.length == 0">%i18n:mobile.tags.mk-user-overview-posts.no-posts%</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	data() {
+		return {
+			fetching: true,
+			posts: []
+		};
+	},
+	mounted() {
+		this.$root.$data.os.api('users/posts', {
+			user_id: this.user.id
+		}).then(posts => {
+			this.fetching = false;
+			this.posts = posts;
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-home-posts
+
+	> div
+		overflow-x scroll
+		-webkit-overflow-scrolling touch
+		white-space nowrap
+		padding 8px
+
+		> *
+			vertical-align top
+
+			&:not(:last-child)
+				margin-right 8px
+
+	> .initializing
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> i
+			margin-right 4px
+
+</style>
diff --git a/src/web/app/mobile/views/pages/user/home.vue b/src/web/app/mobile/views/pages/user/home.vue
new file mode 100644
index 000000000..56b928559
--- /dev/null
+++ b/src/web/app/mobile/views/pages/user/home.vue
@@ -0,0 +1,95 @@
+<template>
+<div class="mk-user-home">
+	<mk-post-detail v-if="user.pinned_post" :post="user.pinned_post" compact/>
+	<section class="recent-posts">
+		<h2>%fa:R comments%%i18n:mobile.tags.mk-user-overview.recent-posts%</h2>
+		<div>
+			<mk-user-home-posts :user="user"/>
+		</div>
+	</section>
+	<section class="images">
+		<h2>%fa:image%%i18n:mobile.tags.mk-user-overview.images%</h2>
+		<div>
+			<mk-user-home-photos :user="user"/>
+		</div>
+	</section>
+	<section class="activity">
+		<h2>%fa:chart-bar%%i18n:mobile.tags.mk-user-overview.activity%</h2>
+		<div>
+			<mk-user-home-activity-chart :user="user"/>
+		</div>
+	</section>
+	<section class="keywords">
+		<h2>%fa:R comment%%i18n:mobile.tags.mk-user-overview.keywords%</h2>
+		<div>
+			<mk-user-home-keywords :user="user"/>
+		</div>
+	</section>
+	<section class="domains">
+		<h2>%fa:globe%%i18n:mobile.tags.mk-user-overview.domains%</h2>
+		<div>
+			<mk-user-home-domains :user="user"/>
+		</div>
+	</section>
+	<section class="frequently-replied-users">
+		<h2>%fa:users%%i18n:mobile.tags.mk-user-overview.frequently-replied-users%</h2>
+		<div>
+			<mk-user-home-frequently-replied-users :user="user"/>
+		</div>
+	</section>
+	<section class="followers-you-know" v-if="$root.$data.os.isSignedIn && $root.$data.os.i.id !== user.id">
+		<h2>%fa:users%%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2>
+		<div>
+			<mk-user-home-followers-you-know :user="user"/>
+		</div>
+	</section>
+	<p>%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time :time="user.last_used_at"/></b></p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user']
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-home
+	max-width 600px
+	margin 0 auto
+
+	> mk-post-detail
+		margin 0 0 8px 0
+
+	> section
+		background #eee
+		border-radius 8px
+		box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+
+		&:not(:last-child)
+			margin-bottom 8px
+
+		> h2
+			margin 0
+			padding 8px 10px
+			font-size 15px
+			font-weight normal
+			color #465258
+			background #fff
+			border-radius 8px 8px 0 0
+
+			> i
+				margin-right 6px
+
+	> .activity
+		> div
+			padding 8px
+
+	> p
+		display block
+		margin 16px
+		text-align center
+		color #cad2da
+
+</style>

From acfa2ef0288f80b6b972d41c582355306c28ebf5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 18:39:05 +0900
Subject: [PATCH 132/286] wip

---
 src/web/app/common/views/components/index.ts  | 2 ++
 src/web/app/desktop/views/components/index.ts | 2 --
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 3d78e7f9c..48e9e9db0 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -7,6 +7,7 @@ import nav from './nav.vue';
 import postHtml from './post-html';
 import reactionIcon from './reaction-icon.vue';
 import time from './time.vue';
+import images from './images.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
@@ -15,3 +16,4 @@ Vue.component('mk-nav', nav);
 Vue.component('mk-post-html', postHtml);
 Vue.component('mk-reaction-icon', reactionIcon);
 Vue.component('mk-time', time);
+Vue.component('mk-images', images);
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 580c61592..6b58215be 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -20,7 +20,6 @@ import postFormWindow from './post-form-window.vue';
 import repostFormWindow from './repost-form-window.vue';
 import analogClock from './analog-clock.vue';
 import ellipsisIcon from './ellipsis-icon.vue';
-import images from './images.vue';
 import imagesImage from './images-image.vue';
 import imagesImageDialog from './images-image-dialog.vue';
 import notifications from './notifications.vue';
@@ -47,7 +46,6 @@ Vue.component('mk-post-form-window', postFormWindow);
 Vue.component('mk-repost-form-window', repostFormWindow);
 Vue.component('mk-analog-clock', analogClock);
 Vue.component('mk-ellipsis-icon', ellipsisIcon);
-Vue.component('mk-images', images);
 Vue.component('mk-images-image', imagesImage);
 Vue.component('mk-images-image-dialog', imagesImageDialog);
 Vue.component('mk-notifications', notifications);

From 58a6e647b3534262943903ae044eec336877e414 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 19:59:07 +0900
Subject: [PATCH 133/286] wip

---
 package.json                        |  2 ++
 webpack/module/rules/base64.ts      | 13 +++++++------
 webpack/module/rules/fa.ts          | 13 +++++++------
 webpack/module/rules/i18n.ts        | 13 +++++++------
 webpack/module/rules/index.ts       |  6 ++----
 webpack/module/rules/license.ts     | 17 -----------------
 webpack/module/rules/theme-color.ts | 28 ++++++++++++++--------------
 webpack/module/rules/typescript.ts  |  1 +
 webpack/plugins/banner.ts           | 10 ----------
 webpack/plugins/consts.ts           |  6 +++---
 webpack/plugins/index.ts            |  9 +++------
 webpack/webpack.config.ts           |  5 ++++-
 12 files changed, 50 insertions(+), 73 deletions(-)
 delete mode 100644 webpack/module/rules/license.ts
 delete mode 100644 webpack/plugins/banner.ts

diff --git a/package.json b/package.json
index 906d512dc..bf924dcdb 100644
--- a/package.json
+++ b/package.json
@@ -117,6 +117,7 @@
 		"gulp-typescript": "3.2.4",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
+		"hard-source-webpack-plugin": "^0.5.18",
 		"highlight.js": "9.12.0",
 		"html-minifier": "^3.5.9",
 		"inquirer": "5.0.1",
@@ -145,6 +146,7 @@
 		"recaptcha-promise": "0.1.3",
 		"reconnecting-websocket": "3.2.2",
 		"redis": "2.8.0",
+		"replace-string-loader": "0.0.7",
 		"request": "2.83.0",
 		"rimraf": "2.6.2",
 		"riot": "3.8.1",
diff --git a/webpack/module/rules/base64.ts b/webpack/module/rules/base64.ts
index 6d7eaddeb..886f0e8b3 100644
--- a/webpack/module/rules/base64.ts
+++ b/webpack/module/rules/base64.ts
@@ -3,17 +3,18 @@
  */
 
 import * as fs from 'fs';
-const StringReplacePlugin = require('string-replace-webpack-plugin');
 
 export default () => ({
 	enforce: 'pre',
 	test: /\.(vue|js)$/,
 	exclude: /node_modules/,
-	loader: StringReplacePlugin.replace({
-		replacements: [{
-			pattern: /%base64:(.+?)%/g, replacement: (_, key) => {
+	use: [{
+		loader: 'replace-string-loader',
+		options: {
+			search: /%base64:(.+?)%/g,
+			replace: (_, key) => {
 				return fs.readFileSync(__dirname + '/../../../src/web/' + key, 'base64');
 			}
-		}]
-	})
+		}
+	}]
 });
diff --git a/webpack/module/rules/fa.ts b/webpack/module/rules/fa.ts
index 267908923..56ca19d4b 100644
--- a/webpack/module/rules/fa.ts
+++ b/webpack/module/rules/fa.ts
@@ -2,16 +2,17 @@
  * Replace fontawesome symbols
  */
 
-const StringReplacePlugin = require('string-replace-webpack-plugin');
 import { pattern, replacement } from '../../../src/common/build/fa';
 
 export default () => ({
 	enforce: 'pre',
 	test: /\.(vue|js|ts)$/,
 	exclude: /node_modules/,
-	loader: StringReplacePlugin.replace({
-		replacements: [{
-			pattern, replacement
-		}]
-	})
+	use: [{
+		loader: 'replace-string-loader',
+		options: {
+			search: pattern,
+			replace: replacement
+		}
+	}]
 });
diff --git a/webpack/module/rules/i18n.ts b/webpack/module/rules/i18n.ts
index f8063a311..1bd771f43 100644
--- a/webpack/module/rules/i18n.ts
+++ b/webpack/module/rules/i18n.ts
@@ -2,7 +2,6 @@
  * Replace i18n texts
  */
 
-const StringReplacePlugin = require('string-replace-webpack-plugin');
 import Replacer from '../../../src/common/build/i18n';
 
 export default lang => {
@@ -12,10 +11,12 @@ export default lang => {
 		enforce: 'pre',
 		test: /\.(vue|js|ts)$/,
 		exclude: /node_modules/,
-		loader: StringReplacePlugin.replace({
-			replacements: [{
-				pattern: replacer.pattern, replacement: replacer.replacement
-			}]
-		})
+		use: [{
+			loader: 'replace-string-loader',
+			options: {
+				search: replacer.pattern,
+				replace: replacer.replacement
+			}
+		}]
 	};
 };
diff --git a/webpack/module/rules/index.ts b/webpack/module/rules/index.ts
index c63da7112..c4442b06c 100644
--- a/webpack/module/rules/index.ts
+++ b/webpack/module/rules/index.ts
@@ -1,7 +1,6 @@
 import i18n from './i18n';
-import license from './license';
 import fa from './fa';
-import base64 from './base64';
+//import base64 from './base64';
 import themeColor from './theme-color';
 import vue from './vue';
 import stylus from './stylus';
@@ -11,9 +10,8 @@ import collapseSpaces from './collapse-spaces';
 export default lang => [
 	collapseSpaces(),
 	i18n(lang),
-	license(),
 	fa(),
-	base64(),
+	//base64(),
 	themeColor(),
 	vue(),
 	stylus(),
diff --git a/webpack/module/rules/license.ts b/webpack/module/rules/license.ts
deleted file mode 100644
index e3aaefa2b..000000000
--- a/webpack/module/rules/license.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * Inject license
- */
-
-const StringReplacePlugin = require('string-replace-webpack-plugin');
-import { licenseHtml } from '../../../src/common/build/license';
-
-export default () => ({
-	enforce: 'pre',
-	test: /\.(vue|js)$/,
-	exclude: /node_modules/,
-	loader: StringReplacePlugin.replace({
-		replacements: [{
-			pattern: '%license%', replacement: () => licenseHtml
-		}]
-	})
-});
diff --git a/webpack/module/rules/theme-color.ts b/webpack/module/rules/theme-color.ts
index a65338465..14f5457bf 100644
--- a/webpack/module/rules/theme-color.ts
+++ b/webpack/module/rules/theme-color.ts
@@ -2,24 +2,24 @@
  * Theme color provider
  */
 
-const StringReplacePlugin = require('string-replace-webpack-plugin');
-
 const constants = require('../../../src/const.json');
 
 export default () => ({
 	enforce: 'pre',
 	test: /\.vue$/,
 	exclude: /node_modules/,
-	loader: StringReplacePlugin.replace({
-		replacements: [
-			{
-				pattern: /\$theme\-color\-foreground/g,
-				replacement: () => constants.themeColorForeground
-			},
-			{
-				pattern: /\$theme\-color/g,
-				replacement: () => constants.themeColor
-			},
-		]
-	})
+	use: [/*{
+		loader: 'replace-string-loader',
+		options: {
+			search: /\$theme\-color\-foreground/g,
+			replace: constants.themeColorForeground
+		}
+	}, */{
+		loader: 'replace-string-loader',
+		options: {
+			search: '$theme-color',
+			replace: constants.themeColor,
+			flags: 'g'
+		}
+	}]
 });
diff --git a/webpack/module/rules/typescript.ts b/webpack/module/rules/typescript.ts
index 2c9413731..5f2903d77 100644
--- a/webpack/module/rules/typescript.ts
+++ b/webpack/module/rules/typescript.ts
@@ -4,6 +4,7 @@
 
 export default () => ({
 	test: /\.ts$/,
+	exclude: /node_modules/,
 	loader: 'ts-loader',
 	options: {
 		configFile: __dirname + '/../../../src/web/app/tsconfig.json',
diff --git a/webpack/plugins/banner.ts b/webpack/plugins/banner.ts
deleted file mode 100644
index a8774e0a3..000000000
--- a/webpack/plugins/banner.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import * as os from 'os';
-import * as webpack from 'webpack';
-
-export default version => new webpack.BannerPlugin({
-	banner:
-		`Misskey v${version} | MIT Licensed, (c) syuilo 2014-2018\n` +
-		'https://github.com/syuilo/misskey\n' +
-		`built by ${os.hostname()} at ${new Date()}\n` +
-		'hash:[hash], chunkhash:[chunkhash]'
-});
diff --git a/webpack/plugins/consts.ts b/webpack/plugins/consts.ts
index 16a569162..a01c18af6 100644
--- a/webpack/plugins/consts.ts
+++ b/webpack/plugins/consts.ts
@@ -7,6 +7,7 @@ import * as webpack from 'webpack';
 import version from '../../src/version';
 const constants = require('../../src/const.json');
 import config from '../../src/conf';
+import { licenseHtml } from '../../src/common/build/license';
 
 export default lang => {
 	const consts = {
@@ -24,6 +25,7 @@ export default lang => {
 		_LANG_: lang,
 		_HOST_: config.host,
 		_URL_: config.url,
+		_LICENSE_: licenseHtml
 	};
 
 	const _consts = {};
@@ -32,7 +34,5 @@ export default lang => {
 		_consts[key] = JSON.stringify(consts[key]);
 	});
 
-	return new webpack.DefinePlugin(Object.assign({}, _consts, {
-		__CONSTS__: JSON.stringify(consts)
-	}));
+	return new webpack.DefinePlugin(_consts);
 };
diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
index d97f78155..a29d2b7e2 100644
--- a/webpack/plugins/index.ts
+++ b/webpack/plugins/index.ts
@@ -1,17 +1,16 @@
-const StringReplacePlugin = require('string-replace-webpack-plugin');
+const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
 
 import consts from './consts';
 import hoist from './hoist';
 import minify from './minify';
-import banner from './banner';
 
 const env = process.env.NODE_ENV;
 const isProduction = env === 'production';
 
 export default (version, lang) => {
 	const plugins = [
-		consts(lang),
-		new StringReplacePlugin()
+		new HardSourceWebpackPlugin(),
+		consts(lang)
 	];
 
 	if (isProduction) {
@@ -19,7 +18,5 @@ export default (version, lang) => {
 		plugins.push(minify());
 	}
 
-	plugins.push(banner(version));
-
 	return plugins;
 };
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 1a516d141..f4b9247e6 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -40,6 +40,9 @@ module.exports = Object.keys(langs).map(lang => {
 				'.js', '.ts'
 			]
 		},
-		cache: true
+		cache: true,
+		devtool: 'eval',
+		stats: true,
+		profile: true
 	};
 });

From bac7192788d35e6304f19e8fde935e372c3e22bb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 20:00:23 +0900
Subject: [PATCH 134/286] wip

---
 webpack/module/rules/theme-color.ts | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/webpack/module/rules/theme-color.ts b/webpack/module/rules/theme-color.ts
index 14f5457bf..4828e00ec 100644
--- a/webpack/module/rules/theme-color.ts
+++ b/webpack/module/rules/theme-color.ts
@@ -8,13 +8,14 @@ export default () => ({
 	enforce: 'pre',
 	test: /\.vue$/,
 	exclude: /node_modules/,
-	use: [/*{
+	use: [{
 		loader: 'replace-string-loader',
 		options: {
-			search: /\$theme\-color\-foreground/g,
-			replace: constants.themeColorForeground
+			search: '$theme-color-foreground',
+			replace: constants.themeColorForeground,
+			flags: 'g'
 		}
-	}, */{
+	}, {
 		loader: 'replace-string-loader',
 		options: {
 			search: '$theme-color',

From a215ef66808d29e7040daf5c58fa2395539e20f4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 21:29:59 +0900
Subject: [PATCH 135/286] wip

---
 src/web/app/mobile/tags/notify.tag            | 40 ---------------
 .../app/mobile/views/components/notify.vue    | 49 +++++++++++++++++++
 2 files changed, 49 insertions(+), 40 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/notify.tag
 create mode 100644 src/web/app/mobile/views/components/notify.vue

diff --git a/src/web/app/mobile/tags/notify.tag b/src/web/app/mobile/tags/notify.tag
deleted file mode 100644
index ec3609497..000000000
--- a/src/web/app/mobile/tags/notify.tag
+++ /dev/null
@@ -1,40 +0,0 @@
-<mk-notify>
-	<mk-notification-preview notification={ opts.notification }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			position fixed
-			z-index 1024
-			bottom -64px
-			left 0
-			width 100%
-			height 64px
-			pointer-events none
-			-webkit-backdrop-filter blur(2px)
-			backdrop-filter blur(2px)
-			background-color rgba(#000, 0.5)
-
-	</style>
-	<script lang="typescript">
-		import * as anime from 'animejs';
-
-		this.on('mount', () => {
-			anime({
-				targets: this.root,
-				bottom: '0px',
-				duration: 500,
-				easing: 'easeOutQuad'
-			});
-
-			setTimeout(() => {
-				anime({
-					targets: this.root,
-					bottom: '-64px',
-					duration: 500,
-					easing: 'easeOutQuad',
-					complete: () => this.$destroy()
-				});
-			}, 6000);
-		});
-	</script>
-</mk-notify>
diff --git a/src/web/app/mobile/views/components/notify.vue b/src/web/app/mobile/views/components/notify.vue
new file mode 100644
index 000000000..d3e09e450
--- /dev/null
+++ b/src/web/app/mobile/views/components/notify.vue
@@ -0,0 +1,49 @@
+<template>
+<div class="mk-notify">
+	<mk-notification-preview :notification="notification"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+
+export default Vue.extend({
+	props: ['notification'],
+	mounted() {
+		Vue.nextTick(() => {
+			anime({
+				targets: this.$el,
+				bottom: '0px',
+				duration: 500,
+				easing: 'easeOutQuad'
+			});
+
+			setTimeout(() => {
+				anime({
+					targets: this.$el,
+					bottom: '-64px',
+					duration: 500,
+					easing: 'easeOutQuad',
+					complete: () => this.$destroy()
+				});
+			}, 6000);
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-notify
+	position fixed
+	z-index 1024
+	bottom -64px
+	left 0
+	width 100%
+	height 64px
+	pointer-events none
+	-webkit-backdrop-filter blur(2px)
+	backdrop-filter blur(2px)
+	background-color rgba(#000, 0.5)
+
+</style>

From a7601f7aa493db5d96b66fbbf79e92ff54c4ff28 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 23:07:19 +0900
Subject: [PATCH 136/286] wip

---
 package.json                            |  2 ++
 webpack/module/rules/base64.ts          | 14 ++++++-------
 webpack/module/rules/collapse-spaces.ts | 23 ++++++++++----------
 webpack/module/rules/fa.ts              | 12 +++++------
 webpack/module/rules/i18n.ts            | 12 +++++------
 webpack/module/rules/index.ts           |  8 +++----
 webpack/module/rules/theme-color.ts     | 26 -----------------------
 webpack/module/rules/vue.ts             | 28 ++++++++++++++++++++-----
 8 files changed, 55 insertions(+), 70 deletions(-)
 delete mode 100644 webpack/module/rules/theme-color.ts

diff --git a/package.json b/package.json
index bf924dcdb..06e517a0d 100644
--- a/package.json
+++ b/package.json
@@ -157,6 +157,7 @@
 		"serve-favicon": "2.4.5",
 		"sortablejs": "1.7.0",
 		"speakeasy": "2.0.0",
+		"string-replace-loader": "^1.3.0",
 		"string-replace-webpack-plugin": "0.1.3",
 		"style-loader": "0.20.1",
 		"stylus": "0.54.5",
@@ -182,6 +183,7 @@
 		"vue-template-compiler": "^2.5.13",
 		"web-push": "3.2.5",
 		"webpack": "3.10.0",
+		"webpack-replace-loader": "^1.3.0",
 		"websocket": "1.0.25",
 		"xev": "2.0.0"
 	}
diff --git a/webpack/module/rules/base64.ts b/webpack/module/rules/base64.ts
index 886f0e8b3..c2f6b9339 100644
--- a/webpack/module/rules/base64.ts
+++ b/webpack/module/rules/base64.ts
@@ -8,13 +8,11 @@ export default () => ({
 	enforce: 'pre',
 	test: /\.(vue|js)$/,
 	exclude: /node_modules/,
-	use: [{
-		loader: 'replace-string-loader',
-		options: {
-			search: /%base64:(.+?)%/g,
-			replace: (_, key) => {
-				return fs.readFileSync(__dirname + '/../../../src/web/' + key, 'base64');
-			}
+	loader: 'string-replace-loader',
+	query: {
+		search: /%base64:(.+?)%/g,
+		replace: (_, key) => {
+			return fs.readFileSync(__dirname + '/../../../src/web/' + key, 'base64');
 		}
-	}]
+	}
 });
diff --git a/webpack/module/rules/collapse-spaces.ts b/webpack/module/rules/collapse-spaces.ts
index 48fd57f01..734c73592 100644
--- a/webpack/module/rules/collapse-spaces.ts
+++ b/webpack/module/rules/collapse-spaces.ts
@@ -1,20 +1,19 @@
 import * as fs from 'fs';
 const minify = require('html-minifier').minify;
-const StringReplacePlugin = require('string-replace-webpack-plugin');
 
 export default () => ({
 	enforce: 'pre',
 	test: /\.vue$/,
 	exclude: /node_modules/,
-	loader: StringReplacePlugin.replace({
-		replacements: [{
-			pattern: /^<template>([\s\S]+?)\r?\n<\/template>/, replacement: html => {
-				return minify(html, {
-					collapseWhitespace: true,
-					collapseInlineTagWhitespace: true,
-					keepClosingSlash: true
-				});
-			}
-		}]
-	})
+	loader: 'string-replace-loader',
+	query: {
+		search: /^<template>([\s\S]+?)\r?\n<\/template>/,
+		replace: html => {
+			return minify(html, {
+				collapseWhitespace: true,
+				collapseInlineTagWhitespace: true,
+				keepClosingSlash: true
+			});
+		}
+	}
 });
diff --git a/webpack/module/rules/fa.ts b/webpack/module/rules/fa.ts
index 56ca19d4b..2ac89ce4f 100644
--- a/webpack/module/rules/fa.ts
+++ b/webpack/module/rules/fa.ts
@@ -8,11 +8,9 @@ export default () => ({
 	enforce: 'pre',
 	test: /\.(vue|js|ts)$/,
 	exclude: /node_modules/,
-	use: [{
-		loader: 'replace-string-loader',
-		options: {
-			search: pattern,
-			replace: replacement
-		}
-	}]
+	loader: 'string-replace-loader',
+	query: {
+		search: pattern,
+		replace: replacement
+	}
 });
diff --git a/webpack/module/rules/i18n.ts b/webpack/module/rules/i18n.ts
index 1bd771f43..2352a42be 100644
--- a/webpack/module/rules/i18n.ts
+++ b/webpack/module/rules/i18n.ts
@@ -11,12 +11,10 @@ export default lang => {
 		enforce: 'pre',
 		test: /\.(vue|js|ts)$/,
 		exclude: /node_modules/,
-		use: [{
-			loader: 'replace-string-loader',
-			options: {
-				search: replacer.pattern,
-				replace: replacer.replacement
-			}
-		}]
+		loader: 'string-replace-loader',
+		query: {
+			search: replacer.pattern,
+			replace: replacer.replacement
+		}
 	};
 };
diff --git a/webpack/module/rules/index.ts b/webpack/module/rules/index.ts
index c4442b06c..1ddebacff 100644
--- a/webpack/module/rules/index.ts
+++ b/webpack/module/rules/index.ts
@@ -1,18 +1,16 @@
 import i18n from './i18n';
 import fa from './fa';
 //import base64 from './base64';
-import themeColor from './theme-color';
 import vue from './vue';
 import stylus from './stylus';
 import typescript from './typescript';
 import collapseSpaces from './collapse-spaces';
 
 export default lang => [
-	collapseSpaces(),
-	i18n(lang),
-	fa(),
+	//collapseSpaces(),
+	//i18n(lang),
+	//fa(),
 	//base64(),
-	themeColor(),
 	vue(),
 	stylus(),
 	typescript()
diff --git a/webpack/module/rules/theme-color.ts b/webpack/module/rules/theme-color.ts
deleted file mode 100644
index 4828e00ec..000000000
--- a/webpack/module/rules/theme-color.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Theme color provider
- */
-
-const constants = require('../../../src/const.json');
-
-export default () => ({
-	enforce: 'pre',
-	test: /\.vue$/,
-	exclude: /node_modules/,
-	use: [{
-		loader: 'replace-string-loader',
-		options: {
-			search: '$theme-color-foreground',
-			replace: constants.themeColorForeground,
-			flags: 'g'
-		}
-	}, {
-		loader: 'replace-string-loader',
-		options: {
-			search: '$theme-color',
-			replace: constants.themeColor,
-			flags: 'g'
-		}
-	}]
-});
diff --git a/webpack/module/rules/vue.ts b/webpack/module/rules/vue.ts
index 02d644615..990f83991 100644
--- a/webpack/module/rules/vue.ts
+++ b/webpack/module/rules/vue.ts
@@ -2,12 +2,30 @@
  * Vue
  */
 
+const constants = require('../../../src/const.json');
+
 export default () => ({
 	test: /\.vue$/,
 	exclude: /node_modules/,
-	loader: 'vue-loader',
-	options: {
-		cssSourceMap: false,
-		preserveWhitespace: false
-	}
+	use: [{
+		loader: 'vue-loader',
+		options: {
+			cssSourceMap: false,
+			preserveWhitespace: false
+		}
+	}, {
+		loader: 'webpack-replace-loader',
+		options: {
+			search: '$theme-color',
+			replace: constants.themeColor,
+			attr: 'g'
+		}
+	}, {
+		loader: 'webpack-replace-loader',
+		query: {
+			search: '$theme-color-foreground',
+			replace: constants.themeColorForeground,
+			attr: 'g'
+		}
+	}]
 });

From 085ac938c2641cbb31ec6bf50f0617eafe543ba4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 02:53:54 +0900
Subject: [PATCH 137/286] wip

---
 package.json                  |  1 +
 webpack/loaders/replace.js    | 15 +++++++++++++++
 webpack/module/rules/fa.ts    |  6 +++---
 webpack/module/rules/i18n.ts  |  6 +++---
 webpack/module/rules/index.ts |  5 +++--
 webpack/webpack.config.ts     |  3 +++
 6 files changed, 28 insertions(+), 8 deletions(-)
 create mode 100644 webpack/loaders/replace.js

diff --git a/package.json b/package.json
index 06e517a0d..6df445f29 100644
--- a/package.json
+++ b/package.json
@@ -125,6 +125,7 @@
 		"is-url": "1.2.2",
 		"js-yaml": "3.10.0",
 		"license-checker": "16.0.0",
+		"loader-utils": "^1.1.0",
 		"mecab-async": "0.1.2",
 		"mkdirp": "0.5.1",
 		"mocha": "5.0.0",
diff --git a/webpack/loaders/replace.js b/webpack/loaders/replace.js
new file mode 100644
index 000000000..41c33ce8d
--- /dev/null
+++ b/webpack/loaders/replace.js
@@ -0,0 +1,15 @@
+const loaderUtils = require('loader-utils');
+
+function trim(text) {
+	return text.substring(1, text.length - 2);
+}
+
+module.exports = function(src) {
+	this.cacheable();
+	const options = loaderUtils.getOptions(this);
+	if (typeof options.search != 'string' || options.search.length == 0) console.error('invalid search');
+	if (typeof options.replace != 'function') console.error('invalid replacer');
+	src = src.replace(new RegExp(trim(options.search), 'g'), options.replace);
+	this.callback(null, src);
+	return src;
+};
diff --git a/webpack/module/rules/fa.ts b/webpack/module/rules/fa.ts
index 2ac89ce4f..a31bf1bee 100644
--- a/webpack/module/rules/fa.ts
+++ b/webpack/module/rules/fa.ts
@@ -5,12 +5,12 @@
 import { pattern, replacement } from '../../../src/common/build/fa';
 
 export default () => ({
-	enforce: 'pre',
+	//enforce: 'pre',
 	test: /\.(vue|js|ts)$/,
 	exclude: /node_modules/,
-	loader: 'string-replace-loader',
+	loader: 'replace',
 	query: {
-		search: pattern,
+		search: pattern.toString(),
 		replace: replacement
 	}
 });
diff --git a/webpack/module/rules/i18n.ts b/webpack/module/rules/i18n.ts
index 2352a42be..f3270b4df 100644
--- a/webpack/module/rules/i18n.ts
+++ b/webpack/module/rules/i18n.ts
@@ -8,12 +8,12 @@ export default lang => {
 	const replacer = new Replacer(lang);
 
 	return {
-		enforce: 'pre',
+		//enforce: 'post',
 		test: /\.(vue|js|ts)$/,
 		exclude: /node_modules/,
-		loader: 'string-replace-loader',
+		loader: 'replace',
 		query: {
-			search: replacer.pattern,
+			search: replacer.pattern.toString(),
 			replace: replacer.replacement
 		}
 	};
diff --git a/webpack/module/rules/index.ts b/webpack/module/rules/index.ts
index 1ddebacff..d97614ad2 100644
--- a/webpack/module/rules/index.ts
+++ b/webpack/module/rules/index.ts
@@ -8,10 +8,11 @@ import collapseSpaces from './collapse-spaces';
 
 export default lang => [
 	//collapseSpaces(),
-	//i18n(lang),
-	//fa(),
+
 	//base64(),
 	vue(),
+	i18n(lang),
+	fa(),
 	stylus(),
 	typescript()
 ];
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index f4b9247e6..dd00acaae 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -40,6 +40,9 @@ module.exports = Object.keys(langs).map(lang => {
 				'.js', '.ts'
 			]
 		},
+		resolveLoader: {
+			modules: ['node_modules', './webpack/loaders']
+		},
 		cache: true,
 		devtool: 'eval',
 		stats: true,

From 11f32375b662c7584aeb0efae4513ea07e8bb1b3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 03:23:10 +0900
Subject: [PATCH 138/286] wip

---
 .../desktop/views/components/post-form.vue    |  2 +-
 webpack/loaders/replace.js                    |  8 ++-
 webpack/module/index.ts                       |  5 --
 webpack/module/rules/fa.ts                    | 16 -----
 webpack/module/rules/i18n.ts                  | 20 ------
 webpack/module/rules/index.ts                 | 18 -----
 webpack/module/rules/stylus.ts                | 13 ----
 webpack/module/rules/typescript.ts            | 13 ----
 webpack/module/rules/vue.ts                   | 31 ---------
 webpack/webpack.config.ts                     | 66 ++++++++++++++++++-
 10 files changed, 70 insertions(+), 122 deletions(-)
 delete mode 100644 webpack/module/index.ts
 delete mode 100644 webpack/module/rules/fa.ts
 delete mode 100644 webpack/module/rules/i18n.ts
 delete mode 100644 webpack/module/rules/index.ts
 delete mode 100644 webpack/module/rules/stylus.ts
 delete mode 100644 webpack/module/rules/typescript.ts
 delete mode 100644 webpack/module/rules/vue.ts

diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 91ceb5227..0a5f8812d 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -37,7 +37,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import Sortable from 'sortablejs';
+import * as Sortable from 'sortablejs';
 import Autocomplete from '../../scripts/autocomplete';
 import getKao from '../../../common/scripts/get-kao';
 import notify from '../../scripts/notify';
diff --git a/webpack/loaders/replace.js b/webpack/loaders/replace.js
index 41c33ce8d..4bb00a2ab 100644
--- a/webpack/loaders/replace.js
+++ b/webpack/loaders/replace.js
@@ -7,9 +7,11 @@ function trim(text) {
 module.exports = function(src) {
 	this.cacheable();
 	const options = loaderUtils.getOptions(this);
-	if (typeof options.search != 'string' || options.search.length == 0) console.error('invalid search');
-	if (typeof options.replace != 'function') console.error('invalid replacer');
-	src = src.replace(new RegExp(trim(options.search), 'g'), options.replace);
+	const search = options.search;
+	const replace = global[options.replace];
+	if (typeof search != 'string' || search.length == 0) console.error('invalid search');
+	if (typeof replace != 'function') console.error('invalid replacer:', replace, this.request);
+	src = src.replace(new RegExp(trim(search), 'g'), replace);
 	this.callback(null, src);
 	return src;
 };
diff --git a/webpack/module/index.ts b/webpack/module/index.ts
deleted file mode 100644
index 088aca723..000000000
--- a/webpack/module/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import rules from './rules';
-
-export default lang => ({
-	rules: rules(lang)
-});
diff --git a/webpack/module/rules/fa.ts b/webpack/module/rules/fa.ts
deleted file mode 100644
index a31bf1bee..000000000
--- a/webpack/module/rules/fa.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Replace fontawesome symbols
- */
-
-import { pattern, replacement } from '../../../src/common/build/fa';
-
-export default () => ({
-	//enforce: 'pre',
-	test: /\.(vue|js|ts)$/,
-	exclude: /node_modules/,
-	loader: 'replace',
-	query: {
-		search: pattern.toString(),
-		replace: replacement
-	}
-});
diff --git a/webpack/module/rules/i18n.ts b/webpack/module/rules/i18n.ts
deleted file mode 100644
index f3270b4df..000000000
--- a/webpack/module/rules/i18n.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Replace i18n texts
- */
-
-import Replacer from '../../../src/common/build/i18n';
-
-export default lang => {
-	const replacer = new Replacer(lang);
-
-	return {
-		//enforce: 'post',
-		test: /\.(vue|js|ts)$/,
-		exclude: /node_modules/,
-		loader: 'replace',
-		query: {
-			search: replacer.pattern.toString(),
-			replace: replacer.replacement
-		}
-	};
-};
diff --git a/webpack/module/rules/index.ts b/webpack/module/rules/index.ts
deleted file mode 100644
index d97614ad2..000000000
--- a/webpack/module/rules/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import i18n from './i18n';
-import fa from './fa';
-//import base64 from './base64';
-import vue from './vue';
-import stylus from './stylus';
-import typescript from './typescript';
-import collapseSpaces from './collapse-spaces';
-
-export default lang => [
-	//collapseSpaces(),
-
-	//base64(),
-	vue(),
-	i18n(lang),
-	fa(),
-	stylus(),
-	typescript()
-];
diff --git a/webpack/module/rules/stylus.ts b/webpack/module/rules/stylus.ts
deleted file mode 100644
index dd1e4c321..000000000
--- a/webpack/module/rules/stylus.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * Stylus support
- */
-
-export default () => ({
-	test: /\.styl$/,
-	exclude: /node_modules/,
-	use: [
-		{ loader: 'style-loader' },
-		{ loader: 'css-loader' },
-		{ loader: 'stylus-loader' }
-	]
-});
diff --git a/webpack/module/rules/typescript.ts b/webpack/module/rules/typescript.ts
deleted file mode 100644
index 5f2903d77..000000000
--- a/webpack/module/rules/typescript.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * TypeScript
- */
-
-export default () => ({
-	test: /\.ts$/,
-	exclude: /node_modules/,
-	loader: 'ts-loader',
-	options: {
-		configFile: __dirname + '/../../../src/web/app/tsconfig.json',
-		appendTsSuffixTo: [/\.vue$/]
-	}
-});
diff --git a/webpack/module/rules/vue.ts b/webpack/module/rules/vue.ts
deleted file mode 100644
index 990f83991..000000000
--- a/webpack/module/rules/vue.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Vue
- */
-
-const constants = require('../../../src/const.json');
-
-export default () => ({
-	test: /\.vue$/,
-	exclude: /node_modules/,
-	use: [{
-		loader: 'vue-loader',
-		options: {
-			cssSourceMap: false,
-			preserveWhitespace: false
-		}
-	}, {
-		loader: 'webpack-replace-loader',
-		options: {
-			search: '$theme-color',
-			replace: constants.themeColor,
-			attr: 'g'
-		}
-	}, {
-		loader: 'webpack-replace-loader',
-		query: {
-			search: '$theme-color-foreground',
-			replace: constants.themeColorForeground,
-			attr: 'g'
-		}
-	}]
-});
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index dd00acaae..ee7d4df9e 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -2,12 +2,17 @@
  * webpack configuration
  */
 
-import module_ from './module';
+import I18nReplacer from '../src/common/build/i18n';
+import { pattern as faPattern, replacement as faReplacement } from '../src/common/build/fa';
+const constants = require('../src/const.json');
+
 import plugins from './plugins';
 
 import langs from '../locales';
 import version from '../src/version';
 
+global['faReplacement'] = faReplacement;
+
 module.exports = Object.keys(langs).map(lang => {
 	// Chunk name
 	const name = lang;
@@ -29,10 +34,67 @@ module.exports = Object.keys(langs).map(lang => {
 		filename: `[name].${version}.${lang}.js`
 	};
 
+	const i18nReplacer = new I18nReplacer(lang);
+	global['i18nReplacement'] = i18nReplacer.replacement;
+
 	return {
 		name,
 		entry,
-		module: module_(lang),
+		module: {
+			rules: [{
+				test: /\.vue$/,
+				exclude: /node_modules/,
+				use: [{
+					loader: 'vue-loader',
+					options: {
+						cssSourceMap: false,
+						preserveWhitespace: false
+					}
+				}, {
+					loader: 'webpack-replace-loader',
+					options: {
+						search: '$theme-color',
+						replace: constants.themeColor,
+						attr: 'g'
+					}
+				}, {
+					loader: 'webpack-replace-loader',
+					query: {
+						search: '$theme-color-foreground',
+						replace: constants.themeColorForeground,
+						attr: 'g'
+					}
+				}, {
+					loader: 'replace',
+					query: {
+						search: i18nReplacer.pattern.toString(),
+						replace: 'i18nReplacement'
+					}
+				}, {
+					loader: 'replace',
+					query: {
+						search: faPattern.toString(),
+						replace: 'faReplacement'
+					}
+				}]
+			}, {
+				test: /\.styl$/,
+				exclude: /node_modules/,
+				use: [
+					{ loader: 'style-loader' },
+					{ loader: 'css-loader' },
+					{ loader: 'stylus-loader' }
+				]
+			}, {
+				test: /\.ts$/,
+				exclude: /node_modules/,
+				loader: 'ts-loader',
+				options: {
+					configFile: __dirname + '/../src/web/app/tsconfig.json',
+					appendTsSuffixTo: [/\.vue$/]
+				}
+			}]
+		},
 		plugins: plugins(version, lang),
 		output,
 		resolve: {

From 7e55e6c15943b1aad183dbb24c7bcee86f9fc540 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 03:26:59 +0900
Subject: [PATCH 139/286] wip

---
 webpack/webpack.config.ts | 24 +++++++++++++++++++-----
 1 file changed, 19 insertions(+), 5 deletions(-)

diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index ee7d4df9e..8cdb1738c 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -88,11 +88,25 @@ module.exports = Object.keys(langs).map(lang => {
 			}, {
 				test: /\.ts$/,
 				exclude: /node_modules/,
-				loader: 'ts-loader',
-				options: {
-					configFile: __dirname + '/../src/web/app/tsconfig.json',
-					appendTsSuffixTo: [/\.vue$/]
-				}
+				use: [{
+					loader: 'ts-loader',
+					options: {
+						configFile: __dirname + '/../src/web/app/tsconfig.json',
+						appendTsSuffixTo: [/\.vue$/]
+					}
+				}, {
+					loader: 'replace',
+					query: {
+						search: i18nReplacer.pattern.toString(),
+						replace: 'i18nReplacement'
+					}
+				}, {
+					loader: 'replace',
+					query: {
+						search: faPattern.toString(),
+						replace: 'faReplacement'
+					}
+				}]
 			}]
 		},
 		plugins: plugins(version, lang),

From 5f5d7b893578224bb4a9077c9bb474d47f6768d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 05:24:23 +0900
Subject: [PATCH 140/286] wip

---
 src/web/app/desktop/-tags/pages/home.tag | 54 ------------------------
 src/web/app/desktop/views/pages/home.vue | 41 ++++++++++++++++++
 2 files changed, 41 insertions(+), 54 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/pages/home.tag

diff --git a/src/web/app/desktop/-tags/pages/home.tag b/src/web/app/desktop/-tags/pages/home.tag
deleted file mode 100644
index 83ceb3846..000000000
--- a/src/web/app/desktop/-tags/pages/home.tag
+++ /dev/null
@@ -1,54 +0,0 @@
-<mk-home-page>
-	<mk-ui ref="ui" page={ page }>
-		<mk-home ref="home" mode={ parent.opts.mode }/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import Progress from '../../../common/scripts/loading';
-		import getPostSummary from '../../../../../common/get-post-summary.ts';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.unreadCount = 0;
-		this.page = this.opts.mode || 'timeline';
-
-		this.on('mount', () => {
-			this.$refs.ui.refs.home.on('loaded', () => {
-				Progress.done();
-			});
-			document.title = 'Misskey';
-			Progress.start();
-
-			this.connection.on('post', this.onStreamPost);
-			document.addEventListener('visibilitychange', this.windowOnVisibilitychange, false);
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('post', this.onStreamPost);
-			this.stream.dispose(this.connectionId);
-			document.removeEventListener('visibilitychange', this.windowOnVisibilitychange);
-		});
-
-		this.onStreamPost = post => {
-			if (document.hidden && post.user_id != this.$root.$data.os.i.id) {
-				this.unreadCount++;
-				document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
-			}
-		};
-
-		this.windowOnVisibilitychange = () => {
-			if (!document.hidden) {
-				this.unreadCount = 0;
-				document.title = 'Misskey';
-			}
-		};
-	</script>
-</mk-home-page>
diff --git a/src/web/app/desktop/views/pages/home.vue b/src/web/app/desktop/views/pages/home.vue
index 2dd7f47a4..7dc234ac0 100644
--- a/src/web/app/desktop/views/pages/home.vue
+++ b/src/web/app/desktop/views/pages/home.vue
@@ -6,6 +6,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+import getPostSummary from '../../../../../common/get-post-summary';
+
 export default Vue.extend({
 	props: {
 		mode: {
@@ -13,5 +16,43 @@ export default Vue.extend({
 			default: 'timeline'
 		}
 	},
+	data() {
+		return {
+			connection: null,
+			connectionId: null,
+			unreadCount: 0
+		};
+	},
+	mounted() {
+		document.title = 'Misskey';
+
+		this.connection = this.$root.$data.os.stream.getConnection();
+		this.connectionId = this.$root.$data.os.stream.use();
+
+		this.connection.on('post', this.onStreamPost);
+		document.addEventListener('visibilitychange', this.onVisibilitychange, false);
+
+		Progress.start();
+	},
+	beforeDestroy() {
+		this.connection.off('post', this.onStreamPost);
+		this.$root.$data.os.stream.dispose(this.connectionId);
+		document.removeEventListener('visibilitychange', this.onVisibilitychange);
+	},
+	methods: {
+		onStreamPost(post) {
+			if (document.hidden && post.user_id != this.$root.$data.os.i.id) {
+				this.unreadCount++;
+				document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
+			}
+		},
+
+		onVisibilitychange() {
+			if (!document.hidden) {
+				this.unreadCount = 0;
+				document.title = 'Misskey';
+			}
+		}
+	}
 });
 </script>

From 5883fbad581cf37eceee8140d0cfd9f1d20f3a50 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 05:32:21 +0900
Subject: [PATCH 141/286] wip

---
 src/web/app/desktop/-tags/pages/not-found.tag | 11 ----
 src/web/app/desktop/-tags/pages/post.tag      | 58 ------------------
 src/web/app/desktop/views/pages/post.vue      | 59 +++++++++++++++++++
 3 files changed, 59 insertions(+), 69 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/pages/not-found.tag
 delete mode 100644 src/web/app/desktop/-tags/pages/post.tag
 create mode 100644 src/web/app/desktop/views/pages/post.vue

diff --git a/src/web/app/desktop/-tags/pages/not-found.tag b/src/web/app/desktop/-tags/pages/not-found.tag
deleted file mode 100644
index f2b4ef09a..000000000
--- a/src/web/app/desktop/-tags/pages/not-found.tag
+++ /dev/null
@@ -1,11 +0,0 @@
-<mk-not-found>
-	<mk-ui>
-		<main>
-			<h1>Not Found</h1>
-		</main>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-</mk-not-found>
diff --git a/src/web/app/desktop/-tags/pages/post.tag b/src/web/app/desktop/-tags/pages/post.tag
deleted file mode 100644
index baec48c0a..000000000
--- a/src/web/app/desktop/-tags/pages/post.tag
+++ /dev/null
@@ -1,58 +0,0 @@
-<mk-post-page>
-	<mk-ui ref="ui">
-		<main v-if="!parent.fetching">
-			<a v-if="parent.post.next" href={ parent.post.next }>%fa:angle-up%%i18n:desktop.tags.mk-post-page.next%</a>
-			<mk-post-detail ref="detail" post={ parent.post }/>
-			<a v-if="parent.post.prev" href={ parent.post.prev }>%fa:angle-down%%i18n:desktop.tags.mk-post-page.prev%</a>
-		</main>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			main
-				padding 16px
-				text-align center
-
-				> a
-					display inline-block
-
-					&:first-child
-						margin-bottom 4px
-
-					&:last-child
-						margin-top 4px
-
-					> [data-fa]
-						margin-right 4px
-
-				> mk-post-detail
-					margin 0 auto
-					width 640px
-
-	</style>
-	<script lang="typescript">
-		import Progress from '../../../common/scripts/loading';
-
-		this.mixin('api');
-
-		this.fetching = true;
-		this.post = null;
-
-		this.on('mount', () => {
-			Progress.start();
-
-			this.$root.$data.os.api('posts/show', {
-				post_id: this.opts.post
-			}).then(post => {
-
-				this.update({
-					fetching: false,
-					post: post
-				});
-
-				Progress.done();
-			});
-		});
-	</script>
-</mk-post-page>
diff --git a/src/web/app/desktop/views/pages/post.vue b/src/web/app/desktop/views/pages/post.vue
new file mode 100644
index 000000000..471f5a5c6
--- /dev/null
+++ b/src/web/app/desktop/views/pages/post.vue
@@ -0,0 +1,59 @@
+<template>
+<mk-ui>
+	<main v-if="!fetching">
+		<a v-if="post.next" :href="post.next">%fa:angle-up%%i18n:desktop.tags.mk-post-page.next%</a>
+		<mk-post-detail :post="post"/>
+		<a v-if="post.prev" :href="post.prev">%fa:angle-down%%i18n:desktop.tags.mk-post-page.prev%</a>
+	</main>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+	props: ['postId'],
+	data() {
+		return {
+			fetching: true,
+			post: null
+		};
+	},
+	mounted() {
+		Progress.start();
+
+		this.$root.$data.os.api('posts/show', {
+			post_id: this.postId
+		}).then(post => {
+			this.fetching = false;
+			this.post = post;
+
+			Progress.done();
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+main
+	padding 16px
+	text-align center
+
+	> a
+		display inline-block
+
+		&:first-child
+			margin-bottom 4px
+
+		&:last-child
+			margin-top 4px
+
+		> [data-fa]
+			margin-right 4px
+
+	> .mk-post-detail
+		margin 0 auto
+		width 640px
+
+</style>

From 6e6903237fc93f175e59818d13a9a4c95b325b3c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 05:34:00 +0900
Subject: [PATCH 142/286] wip

---
 webpack/webpack.config.ts | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 8cdb1738c..2b66dd7f7 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -120,8 +120,6 @@ module.exports = Object.keys(langs).map(lang => {
 			modules: ['node_modules', './webpack/loaders']
 		},
 		cache: true,
-		devtool: 'eval',
-		stats: true,
-		profile: true
+		devtool: 'eval'
 	};
 });

From 9d9afaa00f49631d56bccda71116aa23a804d0b0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 05:42:40 +0900
Subject: [PATCH 143/286] wip

---
 src/web/app/mobile/tags/user-preview.tag      |  95 ----------------
 .../mobile/views/components/user-preview.vue  | 103 ++++++++++++++++++
 2 files changed, 103 insertions(+), 95 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/user-preview.tag
 create mode 100644 src/web/app/mobile/views/components/user-preview.vue

diff --git a/src/web/app/mobile/tags/user-preview.tag b/src/web/app/mobile/tags/user-preview.tag
deleted file mode 100644
index ec06365e0..000000000
--- a/src/web/app/mobile/tags/user-preview.tag
+++ /dev/null
@@ -1,95 +0,0 @@
-<mk-user-preview>
-	<a class="avatar-anchor" href={ '/' + user.username }>
-		<img class="avatar" src={ user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-	</a>
-	<div class="main">
-		<header>
-			<a class="name" href={ '/' + user.username }>{ user.name }</a>
-			<span class="username">@{ user.username }</span>
-		</header>
-		<div class="body">
-			<div class="description">{ user.description }</div>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 16px
-			font-size 12px
-
-			@media (min-width 350px)
-				font-size 14px
-
-			@media (min-width 500px)
-				font-size 16px
-
-			&:after
-				content ""
-				display block
-				clear both
-
-			> .avatar-anchor
-				display block
-				float left
-				margin 0 10px 0 0
-
-				@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
-
-			> .main
-				float left
-				width calc(100% - 58px)
-
-				@media (min-width 500px)
-					width calc(100% - 74px)
-
-				> header
-					@media (min-width 500px)
-						margin-bottom 2px
-
-					> .name
-						display inline
-						margin 0
-						padding 0
-						color #777
-						font-size 1em
-						font-weight 700
-						text-align left
-						text-decoration none
-
-						&:hover
-							text-decoration underline
-
-					> .username
-						text-align left
-						margin 0 0 0 8px
-						color #ccc
-
-				> .body
-
-					> .description
-						cursor default
-						display block
-						margin 0
-						padding 0
-						overflow-wrap break-word
-						font-size 1.1em
-						color #717171
-
-	</style>
-	<script lang="typescript">this.user = this.opts.user</script>
-</mk-user-preview>
diff --git a/src/web/app/mobile/views/components/user-preview.vue b/src/web/app/mobile/views/components/user-preview.vue
new file mode 100644
index 000000000..0246cac6a
--- /dev/null
+++ b/src/web/app/mobile/views/components/user-preview.vue
@@ -0,0 +1,103 @@
+<template>
+<div class="mk-user-preview">
+	<a class="avatar-anchor" :href="`/${user.username}`">
+		<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+	</a>
+	<div class="main">
+		<header>
+			<a class="name" :href="`/${user.username}`">{{ user.name }}</a>
+			<span class="username">@{{ user.username }}</span>
+		</header>
+		<div class="body">
+			<div class="description">{{ user.description }}</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user']
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-preview
+	margin 0
+	padding 16px
+	font-size 12px
+
+	@media (min-width 350px)
+		font-size 14px
+
+	@media (min-width 500px)
+		font-size 16px
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	> .avatar-anchor
+		display block
+		float left
+		margin 0 10px 0 0
+
+		@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
+
+	> .main
+		float left
+		width calc(100% - 58px)
+
+		@media (min-width 500px)
+			width calc(100% - 74px)
+
+		> header
+			@media (min-width 500px)
+				margin-bottom 2px
+
+			> .name
+				display inline
+				margin 0
+				padding 0
+				color #777
+				font-size 1em
+				font-weight 700
+				text-align left
+				text-decoration none
+
+				&:hover
+					text-decoration underline
+
+			> .username
+				text-align left
+				margin 0 0 0 8px
+				color #ccc
+
+		> .body
+
+			> .description
+				cursor default
+				display block
+				margin 0
+				padding 0
+				overflow-wrap break-word
+				font-size 1.1em
+				color #717171
+
+</style>

From 3494b31b435e3a2f7d28d84da922f4988e499b63 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 05:50:30 +0900
Subject: [PATCH 144/286] wip

---
 src/web/app/mobile/tags/page/post.tag   | 76 -------------------------
 src/web/app/mobile/views/pages/post.vue | 76 +++++++++++++++++++++++++
 2 files changed, 76 insertions(+), 76 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/page/post.tag
 create mode 100644 src/web/app/mobile/views/pages/post.vue

diff --git a/src/web/app/mobile/tags/page/post.tag b/src/web/app/mobile/tags/page/post.tag
deleted file mode 100644
index ed7cb5254..000000000
--- a/src/web/app/mobile/tags/page/post.tag
+++ /dev/null
@@ -1,76 +0,0 @@
-<mk-post-page>
-	<mk-ui ref="ui">
-		<main v-if="!parent.fetching">
-			<a v-if="parent.post.next" href={ parent.post.next }>%fa:angle-up%%i18n:mobile.tags.mk-post-page.next%</a>
-			<div>
-				<mk-post-detail ref="post" post={ parent.post }/>
-			</div>
-			<a v-if="parent.post.prev" href={ parent.post.prev }>%fa:angle-down%%i18n:mobile.tags.mk-post-page.prev%</a>
-		</main>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			main
-				text-align center
-
-				> div
-					margin 8px auto
-					padding 0
-					max-width 500px
-					width calc(100% - 16px)
-
-					@media (min-width 500px)
-						margin 16px auto
-						width calc(100% - 32px)
-
-				> a
-					display inline-block
-
-					&:first-child
-						margin-top 8px
-
-						@media (min-width 500px)
-							margin-top 16px
-
-					&:last-child
-						margin-bottom 8px
-
-						@media (min-width 500px)
-							margin-bottom 16px
-
-					> [data-fa]
-						margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		import ui from '../../scripts/ui-event';
-		import Progress from '../../../common/scripts/loading';
-
-		this.mixin('api');
-
-		this.fetching = true;
-		this.post = null;
-
-		this.on('mount', () => {
-			document.title = 'Misskey';
-			ui.trigger('title', '%fa:R sticky-note%%i18n:mobile.tags.mk-post-page.title%');
-			document.documentElement.style.background = '#313a42';
-
-			Progress.start();
-
-			this.$root.$data.os.api('posts/show', {
-				post_id: this.opts.post
-			}).then(post => {
-
-				this.update({
-					fetching: false,
-					post: post
-				});
-
-				Progress.done();
-			});
-		});
-	</script>
-</mk-post-page>
diff --git a/src/web/app/mobile/views/pages/post.vue b/src/web/app/mobile/views/pages/post.vue
new file mode 100644
index 000000000..f291a489b
--- /dev/null
+++ b/src/web/app/mobile/views/pages/post.vue
@@ -0,0 +1,76 @@
+<template>
+<mk-ui>
+	<span slot="header">%fa:R sticky-note%%i18n:mobile.tags.mk-post-page.title%</span>
+	<main v-if="!fetching">
+		<a v-if="post.next" :href="post.next">%fa:angle-up%%i18n:mobile.tags.mk-post-page.next%</a>
+		<div>
+			<mk-post-detail :post="parent.post"/>
+		</div>
+		<a v-if="post.prev" :href="post.prev">%fa:angle-down%%i18n:mobile.tags.mk-post-page.prev%</a>
+	</main>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+	props: ['postId'],
+	data() {
+		return {
+			fetching: true,
+			post: null
+		};
+	},
+	mounted() {
+		document.title = 'Misskey';
+		document.documentElement.style.background = '#313a42';
+
+		Progress.start();
+
+		this.$root.$data.os.api('posts/show', {
+			post_id: this.postId
+		}).then(post => {
+			this.fetching = false;
+			this.post = post;
+
+			Progress.done();
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+main
+	text-align center
+
+	> div
+		margin 8px auto
+		padding 0
+		max-width 500px
+		width calc(100% - 16px)
+
+		@media (min-width 500px)
+			margin 16px auto
+			width calc(100% - 32px)
+
+	> a
+		display inline-block
+
+		&:first-child
+			margin-top 8px
+
+			@media (min-width 500px)
+				margin-top 16px
+
+		&:last-child
+			margin-bottom 8px
+
+			@media (min-width 500px)
+				margin-bottom 16px
+
+		> [data-fa]
+			margin-right 4px
+
+</style>

From 9c91ec07dbb2e1b4406b722ced37653b4af108d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 12:33:34 +0900
Subject: [PATCH 145/286] wip

---
 .../desktop/views/components/post-detail.vue  |   2 +-
 src/web/app/mobile/tags/post-detail.tag       | 448 ------------------
 .../views/components/post-detail-sub.vue      | 103 ++++
 .../mobile/views/components/post-detail.vue   | 331 +++++++++++++
 4 files changed, 435 insertions(+), 449 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/post-detail.tag
 create mode 100644 src/web/app/mobile/views/components/post-detail-sub.vue
 create mode 100644 src/web/app/mobile/views/components/post-detail.vue

diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index 090a5bef6..6c36f06fa 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -68,7 +68,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import dateStringify from '../../common/scripts/date-stringify';
+import dateStringify from '../../../common/scripts/date-stringify';
 
 export default Vue.extend({
 	props: {
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
deleted file mode 100644
index 4b8566f96..000000000
--- a/src/web/app/mobile/tags/post-detail.tag
+++ /dev/null
@@ -1,448 +0,0 @@
-<mk-post-detail>
-	<button class="read-more" v-if="p.reply && p.reply.reply_id && context == null" @click="loadContext" disabled={ loadingContext }>
-		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
-		<template v-if="contextFetching">%fa:spinner .pulse%</template>
-	</button>
-	<div class="context">
-		<template each={ post in context }>
-			<mk-post-detail-sub post={ post }/>
-		</template>
-	</div>
-	<div class="reply-to" v-if="p.reply">
-		<mk-post-detail-sub post={ p.reply }/>
-	</div>
-	<div class="repost" v-if="isRepost">
-		<p>
-			<a class="avatar-anchor" href={ '/' + post.user.username }>
-				<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=32' } alt="avatar"/></a>
-				%fa:retweet%<a class="name" href={ '/' + post.user.username }>
-				{ post.user.name }
-			</a>
-			がRepost
-		</p>
-	</div>
-	<article>
-		<header>
-			<a class="avatar-anchor" href={ '/' + p.user.username }>
-				<img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-			</a>
-			<div>
-				<a class="name" href={ '/' + p.user.username }>{ p.user.name }</a>
-				<span class="username">@{ p.user.username }</span>
-			</div>
-		</header>
-		<div class="body">
-			<div class="text" ref="text"></div>
-			<div class="media" v-if="p.media">
-				<mk-images images={ p.media }/>
-			</div>
-			<mk-poll v-if="p.poll" post={ p }/>
-		</div>
-		<a class="time" href={ '/' + p.user.username + '/' + p.id }>
-			<mk-time time={ p.created_at } mode="detail"/>
-		</a>
-		<footer>
-			<mk-reactions-viewer post={ p }/>
-			<button @click="reply" title="%i18n:mobile.tags.mk-post-detail.reply%">
-				%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
-			</button>
-			<button @click="repost" title="Repost">
-				%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
-			</button>
-			<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-post-detail.reaction%">
-				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
-			</button>
-			<button @click="menu" ref="menuButton">
-				%fa:ellipsis-h%
-			</button>
-		</footer>
-	</article>
-	<div class="replies" v-if="!compact">
-		<template each={ post in replies }>
-			<mk-post-detail-sub post={ post }/>
-		</template>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow hidden
-			margin 0 auto
-			padding 0
-			width 100%
-			text-align left
-			background #fff
-			border-radius 8px
-			box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
-
-			> .fetching
-				padding 64px 0
-
-			> .read-more
-				display block
-				margin 0
-				padding 10px 0
-				width 100%
-				font-size 1em
-				text-align center
-				color #999
-				cursor pointer
-				background #fafafa
-				outline none
-				border none
-				border-bottom solid 1px #eef0f2
-				border-radius 6px 6px 0 0
-				box-shadow none
-
-				&:hover
-					background #f6f6f6
-
-				&:active
-					background #f0f0f0
-
-				&:disabled
-					color #ccc
-
-			> .context
-				> *
-					border-bottom 1px solid #eef0f2
-
-			> .repost
-				color #9dbb00
-				background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
-
-				> p
-					margin 0
-					padding 16px 32px
-
-					.avatar-anchor
-						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
-
-					[data-fa]
-						margin-right 4px
-
-					.name
-						font-weight bold
-
-				& + article
-					padding-top 8px
-
-			> .reply-to
-				border-bottom 1px solid #eef0f2
-
-			> article
-				padding 14px 16px 9px 16px
-
-				@media (min-width 500px)
-					padding 28px 32px 18px 32px
-
-				&:after
-					content ""
-					display block
-					clear both
-
-				&:hover
-					> .main > footer > button
-						color #888
-
-				> header
-					display flex
-					line-height 1.1
-
-					> .avatar-anchor
-						display block
-						padding 0 .5em 0 0
-
-						> .avatar
-							display block
-							width 54px
-							height 54px
-							margin 0
-							border-radius 8px
-							vertical-align bottom
-
-							@media (min-width 500px)
-								width 60px
-								height 60px
-
-					> div
-
-						> .name
-							display inline-block
-							margin .4em 0
-							color #777
-							font-size 16px
-							font-weight bold
-							text-align left
-							text-decoration none
-
-							&:hover
-								text-decoration underline
-
-						> .username
-							display block
-							text-align left
-							margin 0
-							color #ccc
-
-				> .body
-					padding 8px 0
-
-					> .text
-						cursor default
-						display block
-						margin 0
-						padding 0
-						overflow-wrap break-word
-						font-size 16px
-						color #717171
-
-						@media (min-width 500px)
-							font-size 24px
-
-						> mk-url-preview
-							margin-top 8px
-
-					> .media
-						> img
-							display block
-							max-width 100%
-
-				> .time
-					font-size 16px
-					color #c0c0c0
-
-				> footer
-					font-size 1.2em
-
-					> button
-						margin 0
-						padding 8px
-						background transparent
-						border none
-						box-shadow none
-						font-size 1em
-						color #ddd
-						cursor pointer
-
-						&:not(:last-child)
-							margin-right 28px
-
-						&:hover
-							color #666
-
-						> .count
-							display inline
-							margin 0 0 0 8px
-							color #999
-
-						&.reacted
-							color $theme-color
-
-			> .replies
-				> *
-					border-top 1px solid #eef0f2
-
-	</style>
-	<script lang="typescript">
-		import compile from '../../common/scripts/text-compiler';
-		import getPostSummary from '../../../../common/get-post-summary.ts';
-		import openPostForm from '../scripts/open-post-form';
-
-		this.mixin('api');
-
-		this.compact = this.opts.compact;
-		this.post = this.opts.post;
-		this.isRepost = this.post.repost != null;
-		this.p = this.isRepost ? this.post.repost : this.post;
-		this.p.reactions_count = this.p.reaction_counts ? Object.keys(this.p.reaction_counts).map(key => this.p.reaction_counts[key]).reduce((a, b) => a + b) : 0;
-		this.summary = getPostSummary(this.p);
-
-		this.loadingContext = false;
-		this.context = null;
-
-		this.on('mount', () => {
-			if (this.p.text) {
-				const tokens = this.p.ast;
-
-				this.$refs.text.innerHTML = compile(tokens);
-
-				Array.from(this.$refs.text.children).forEach(e => {
-					if (e.tagName == 'MK-URL') riot.mount(e);
-				});
-
-				// URLをプレビュー
-				tokens
-				.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
-				.map(t => {
-					riot.mount(this.$refs.text.appendChild(document.createElement('mk-url-preview')), {
-						url: t.url
-					});
-				});
-			}
-
-			// Get replies
-			if (!this.compact) {
-				this.$root.$data.os.api('posts/replies', {
-					post_id: this.p.id,
-					limit: 8
-				}).then(replies => {
-					this.update({
-						replies: replies
-					});
-				});
-			}
-		});
-
-		this.reply = () => {
-			openPostForm({
-				reply: this.p
-			});
-		};
-
-		this.repost = () => {
-			const text = window.prompt(`「${this.summary}」をRepost`);
-			if (text == null) return;
-			this.$root.$data.os.api('posts/create', {
-				repost_id: this.p.id,
-				text: text == '' ? undefined : text
-			});
-		};
-
-		this.react = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-reaction-picker')), {
-				source: this.$refs.reactButton,
-				post: this.p,
-				compact: true
-			});
-		};
-
-		this.menu = () => {
-			riot.mount(document.body.appendChild(document.createElement('mk-post-menu')), {
-				source: this.$refs.menuButton,
-				post: this.p,
-				compact: true
-			});
-		};
-
-		this.loadContext = () => {
-			this.contextFetching = true;
-
-			// Fetch context
-			this.$root.$data.os.api('posts/context', {
-				post_id: this.p.reply_id
-			}).then(context => {
-				this.update({
-					contextFetching: false,
-					context: context.reverse()
-				});
-			});
-		};
-	</script>
-</mk-post-detail>
-
-<mk-post-detail-sub>
-	<article>
-		<a class="avatar-anchor" href={ '/' + post.user.username }>
-			<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-		</a>
-		<div class="main">
-			<header>
-				<a class="name" href={ '/' + post.user.username }>{ post.user.name }</a>
-				<span class="username">@{ post.user.username }</span>
-				<a class="time" href={ '/' + post.user.username + '/' + post.id }>
-					<mk-time time={ post.created_at }/>
-				</a>
-			</header>
-			<div class="body">
-				<mk-sub-post-content class="text" post={ post }/>
-			</div>
-		</div>
-	</article>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 8px
-			font-size 0.9em
-			background #fdfdfd
-
-			@media (min-width 500px)
-				padding 12px
-
-			> article
-				&:after
-					content ""
-					display block
-					clear both
-
-				&:hover
-					> .main > footer > button
-						color #888
-
-				> .avatar-anchor
-					display block
-					float left
-					margin 0 12px 0 0
-
-					> .avatar
-						display block
-						width 48px
-						height 48px
-						margin 0
-						border-radius 8px
-						vertical-align bottom
-
-				> .main
-					float left
-					width calc(100% - 60px)
-
-					> header
-						display flex
-						margin-bottom 4px
-						white-space nowrap
-
-						> .name
-							display block
-							margin 0 .5em 0 0
-							padding 0
-							overflow hidden
-							color #607073
-							font-size 1em
-							font-weight 700
-							text-align left
-							text-decoration none
-							text-overflow ellipsis
-
-							&:hover
-								text-decoration underline
-
-						> .username
-							text-align left
-							margin 0 .5em 0 0
-							color #d1d8da
-
-						> .time
-							margin-left auto
-							color #b2b8bb
-
-					> .body
-
-						> .text
-							cursor default
-							margin 0
-							padding 0
-							font-size 1.1em
-							color #717171
-
-	</style>
-	<script lang="typescript">this.post = this.opts.post</script>
-</mk-post-detail-sub>
diff --git a/src/web/app/mobile/views/components/post-detail-sub.vue b/src/web/app/mobile/views/components/post-detail-sub.vue
new file mode 100644
index 000000000..8836bb1b3
--- /dev/null
+++ b/src/web/app/mobile/views/components/post-detail-sub.vue
@@ -0,0 +1,103 @@
+<template>
+<div class="mk-post-detail-sub">
+	<a class="avatar-anchor" href={ '/' + post.user.username }>
+		<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
+	</a>
+	<div class="main">
+		<header>
+			<a class="name" href={ '/' + post.user.username }>{ post.user.name }</a>
+			<span class="username">@{ post.user.username }</span>
+			<a class="time" href={ '/' + post.user.username + '/' + post.id }>
+				<mk-time time={ post.created_at }/>
+			</a>
+		</header>
+		<div class="body">
+			<mk-sub-post-content class="text" post={ post }/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['post']
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-post-detail-sub
+	margin 0
+	padding 8px
+	font-size 0.9em
+	background #fdfdfd
+
+	@media (min-width 500px)
+		padding 12px
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	&:hover
+		> .main > footer > button
+			color #888
+
+	> .avatar-anchor
+		display block
+		float left
+		margin 0 12px 0 0
+
+		> .avatar
+			display block
+			width 48px
+			height 48px
+			margin 0
+			border-radius 8px
+			vertical-align bottom
+
+	> .main
+		float left
+		width calc(100% - 60px)
+
+		> header
+			display flex
+			margin-bottom 4px
+			white-space nowrap
+
+			> .name
+				display block
+				margin 0 .5em 0 0
+				padding 0
+				overflow hidden
+				color #607073
+				font-size 1em
+				font-weight 700
+				text-align left
+				text-decoration none
+				text-overflow ellipsis
+
+				&:hover
+					text-decoration underline
+
+			> .username
+				text-align left
+				margin 0 .5em 0 0
+				color #d1d8da
+
+			> .time
+				margin-left auto
+				color #b2b8bb
+
+		> .body
+
+			> .text
+				cursor default
+				margin 0
+				padding 0
+				font-size 1.1em
+				color #717171
+
+</style>
+
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
new file mode 100644
index 000000000..ba28e7be3
--- /dev/null
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -0,0 +1,331 @@
+<template>
+<div class="mk-post-detail">
+	<button class="read-more" v-if="p.reply && p.reply.reply_id && context == null" @click="loadContext" disabled={ loadingContext }>
+		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
+		<template v-if="contextFetching">%fa:spinner .pulse%</template>
+	</button>
+	<div class="context">
+		<template each={ post in context }>
+			<mk-post-detail-sub post={ post }/>
+		</template>
+	</div>
+	<div class="reply-to" v-if="p.reply">
+		<mk-post-detail-sub post={ p.reply }/>
+	</div>
+	<div class="repost" v-if="isRepost">
+		<p>
+			<a class="avatar-anchor" href={ '/' + post.user.username }>
+				<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=32' } alt="avatar"/></a>
+				%fa:retweet%<a class="name" href={ '/' + post.user.username }>
+				{ post.user.name }
+			</a>
+			がRepost
+		</p>
+	</div>
+	<article>
+		<header>
+			<a class="avatar-anchor" href={ '/' + p.user.username }>
+				<img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
+			</a>
+			<div>
+				<a class="name" href={ '/' + p.user.username }>{ p.user.name }</a>
+				<span class="username">@{ p.user.username }</span>
+			</div>
+		</header>
+		<div class="body">
+			<mk-post-html v-if="p.ast" :ast="p.ast" :i="$root.$data.os.i"/>
+			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+			<div class="media" v-if="p.media">
+				<mk-images images={ p.media }/>
+			</div>
+			<mk-poll v-if="p.poll" post={ p }/>
+		</div>
+		<a class="time" href={ '/' + p.user.username + '/' + p.id }>
+			<mk-time time={ p.created_at } mode="detail"/>
+		</a>
+		<footer>
+			<mk-reactions-viewer post={ p }/>
+			<button @click="reply" title="%i18n:mobile.tags.mk-post-detail.reply%">
+				%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
+			</button>
+			<button @click="repost" title="Repost">
+				%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
+			</button>
+			<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-post-detail.reaction%">
+				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
+			</button>
+			<button @click="menu" ref="menuButton">
+				%fa:ellipsis-h%
+			</button>
+		</footer>
+	</article>
+	<div class="replies" v-if="!compact">
+		<template each={ post in replies }>
+			<mk-post-detail-sub post={ post }/>
+		</template>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import getPostSummary from '../../../../common/get-post-summary.ts';
+import openPostForm from '../scripts/open-post-form';
+
+export default Vue.extend({
+	props: {
+		post: {
+			type: Object,
+			required: true
+		},
+		compact: {
+			default: false
+		}
+	},
+	data() {
+		return {
+			context: [],
+			contextFetching: false,
+			replies: [],
+		};
+	},
+	computed: {
+		isRepost(): boolean {
+			return this.post.repost != null;
+		},
+		p(): any {
+			return this.isRepost ? this.post.repost : this.post;
+		},
+		reactionsCount(): number {
+			return this.p.reaction_counts
+				? Object.keys(this.p.reaction_counts)
+					.map(key => this.p.reaction_counts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		urls(): string[] {
+			if (this.p.ast) {
+				return this.p.ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
+		}
+	},
+	mounted() {
+		// Get replies
+		if (!this.compact) {
+			this.$root.$data.os.api('posts/replies', {
+				post_id: this.p.id,
+				limit: 8
+			}).then(replies => {
+				this.replies = replies;
+			});
+		}
+	},
+	methods: {
+		fetchContext() {
+			this.contextFetching = true;
+
+			// Fetch context
+			this.$root.$data.os.api('posts/context', {
+				post_id: this.p.reply_id
+			}).then(context => {
+				this.contextFetching = false;
+				this.context = context.reverse();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-post-detail
+	overflow hidden
+	margin 0 auto
+	padding 0
+	width 100%
+	text-align left
+	background #fff
+	border-radius 8px
+	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+
+	> .fetching
+		padding 64px 0
+
+	> .read-more
+		display block
+		margin 0
+		padding 10px 0
+		width 100%
+		font-size 1em
+		text-align center
+		color #999
+		cursor pointer
+		background #fafafa
+		outline none
+		border none
+		border-bottom solid 1px #eef0f2
+		border-radius 6px 6px 0 0
+		box-shadow none
+
+		&:hover
+			background #f6f6f6
+
+		&:active
+			background #f0f0f0
+
+		&:disabled
+			color #ccc
+
+	> .context
+		> *
+			border-bottom 1px solid #eef0f2
+
+	> .repost
+		color #9dbb00
+		background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+
+		> p
+			margin 0
+			padding 16px 32px
+
+			.avatar-anchor
+				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
+
+			[data-fa]
+				margin-right 4px
+
+			.name
+				font-weight bold
+
+		& + article
+			padding-top 8px
+
+	> .reply-to
+		border-bottom 1px solid #eef0f2
+
+	> article
+		padding 14px 16px 9px 16px
+
+		@media (min-width 500px)
+			padding 28px 32px 18px 32px
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		&:hover
+			> .main > footer > button
+				color #888
+
+		> header
+			display flex
+			line-height 1.1
+
+			> .avatar-anchor
+				display block
+				padding 0 .5em 0 0
+
+				> .avatar
+					display block
+					width 54px
+					height 54px
+					margin 0
+					border-radius 8px
+					vertical-align bottom
+
+					@media (min-width 500px)
+						width 60px
+						height 60px
+
+			> div
+
+				> .name
+					display inline-block
+					margin .4em 0
+					color #777
+					font-size 16px
+					font-weight bold
+					text-align left
+					text-decoration none
+
+					&:hover
+						text-decoration underline
+
+				> .username
+					display block
+					text-align left
+					margin 0
+					color #ccc
+
+		> .body
+			padding 8px 0
+
+			> .text
+				cursor default
+				display block
+				margin 0
+				padding 0
+				overflow-wrap break-word
+				font-size 16px
+				color #717171
+
+				@media (min-width 500px)
+					font-size 24px
+
+				> mk-url-preview
+					margin-top 8px
+
+			> .media
+				> img
+					display block
+					max-width 100%
+
+		> .time
+			font-size 16px
+			color #c0c0c0
+
+		> footer
+			font-size 1.2em
+
+			> button
+				margin 0
+				padding 8px
+				background transparent
+				border none
+				box-shadow none
+				font-size 1em
+				color #ddd
+				cursor pointer
+
+				&:not(:last-child)
+					margin-right 28px
+
+				&:hover
+					color #666
+
+				> .count
+					display inline
+					margin 0 0 0 8px
+					color #999
+
+				&.reacted
+					color $theme-color
+
+	> .replies
+		> *
+			border-top 1px solid #eef0f2
+
+</style>

From 62bbc44cc1274dcc4d7f9dcbfcf15abadefff972 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 12:59:19 +0900
Subject: [PATCH 146/286] wip

---
 src/web/app/mobile/tags/user-followers.tag    |  28 ----
 src/web/app/mobile/tags/user-following.tag    |  28 ----
 src/web/app/mobile/tags/users-list.tag        | 127 ------------------
 .../views/components/user-followers.vue       |  26 ++++
 .../views/components/user-following.vue       |  26 ++++
 .../mobile/views/components/users-list.vue    | 126 +++++++++++++++++
 6 files changed, 178 insertions(+), 183 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/user-followers.tag
 delete mode 100644 src/web/app/mobile/tags/user-following.tag
 delete mode 100644 src/web/app/mobile/tags/users-list.tag
 create mode 100644 src/web/app/mobile/views/components/user-followers.vue
 create mode 100644 src/web/app/mobile/views/components/user-following.vue
 create mode 100644 src/web/app/mobile/views/components/users-list.vue

diff --git a/src/web/app/mobile/tags/user-followers.tag b/src/web/app/mobile/tags/user-followers.tag
deleted file mode 100644
index f3f70b2a6..000000000
--- a/src/web/app/mobile/tags/user-followers.tag
+++ /dev/null
@@ -1,28 +0,0 @@
-<mk-user-followers>
-	<mk-users-list ref="list" fetch={ fetch } count={ user.followers_count } you-know-count={ user.followers_you_know_count } no-users={ '%i18n:mobile.tags.mk-user-followers.no-users%' }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.user = this.opts.user;
-
-		this.fetch = (iknow, limit, cursor, cb) => {
-			this.$root.$data.os.api('users/followers', {
-				user_id: this.user.id,
-				iknow: iknow,
-				limit: limit,
-				cursor: cursor ? cursor : undefined
-			}).then(cb);
-		};
-
-		this.on('mount', () => {
-			this.$refs.list.on('loaded', () => {
-				this.$emit('loaded');
-			});
-		});
-	</script>
-</mk-user-followers>
diff --git a/src/web/app/mobile/tags/user-following.tag b/src/web/app/mobile/tags/user-following.tag
deleted file mode 100644
index b76757143..000000000
--- a/src/web/app/mobile/tags/user-following.tag
+++ /dev/null
@@ -1,28 +0,0 @@
-<mk-user-following>
-	<mk-users-list ref="list" fetch={ fetch } count={ user.following_count } you-know-count={ user.following_you_know_count } no-users={ '%i18n:mobile.tags.mk-user-following.no-users%' }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.user = this.opts.user;
-
-		this.fetch = (iknow, limit, cursor, cb) => {
-			this.$root.$data.os.api('users/following', {
-				user_id: this.user.id,
-				iknow: iknow,
-				limit: limit,
-				cursor: cursor ? cursor : undefined
-			}).then(cb);
-		};
-
-		this.on('mount', () => {
-			this.$refs.list.on('loaded', () => {
-				this.$emit('loaded');
-			});
-		});
-	</script>
-</mk-user-following>
diff --git a/src/web/app/mobile/tags/users-list.tag b/src/web/app/mobile/tags/users-list.tag
deleted file mode 100644
index 84427a18e..000000000
--- a/src/web/app/mobile/tags/users-list.tag
+++ /dev/null
@@ -1,127 +0,0 @@
-<mk-users-list>
-	<nav>
-		<span data-is-active={ mode == 'all' } @click="setMode.bind(this, 'all')">%i18n:mobile.tags.mk-users-list.all%<span>{ opts.count }</span></span>
-		<span v-if="$root.$data.os.isSignedIn && opts.youKnowCount" data-is-active={ mode == 'iknow' } @click="setMode.bind(this, 'iknow')">%i18n:mobile.tags.mk-users-list.known%<span>{ opts.youKnowCount }</span></span>
-	</nav>
-	<div class="users" v-if="!fetching && users.length != 0">
-		<mk-user-preview each={ users } user={ this }/>
-	</div>
-	<button class="more" v-if="!fetching && next != null" @click="more" disabled={ moreFetching }>
-		<span v-if="!moreFetching">%i18n:mobile.tags.mk-users-list.load-more%</span>
-		<span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span></button>
-	<p class="no" v-if="!fetching && users.length == 0">{ opts.noUsers }</p>
-	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> nav
-				display flex
-				justify-content center
-				margin 0 auto
-				max-width 600px
-				border-bottom solid 1px rgba(0, 0, 0, 0.2)
-
-				> span
-					display block
-					flex 1 1
-					text-align center
-					line-height 52px
-					font-size 14px
-					color #657786
-					border-bottom solid 2px transparent
-
-					&[data-is-active]
-						font-weight bold
-						color $theme-color
-						border-color $theme-color
-
-					> span
-						display inline-block
-						margin-left 4px
-						padding 2px 5px
-						font-size 12px
-						line-height 1
-						color #fff
-						background rgba(0, 0, 0, 0.3)
-						border-radius 20px
-
-			> .users
-				margin 8px auto
-				max-width 500px
-				width calc(100% - 16px)
-				background #fff
-				border-radius 8px
-				box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
-
-				@media (min-width 500px)
-					margin 16px auto
-					width calc(100% - 32px)
-
-				> *
-					border-bottom solid 1px rgba(0, 0, 0, 0.05)
-
-			> .no
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-			> .fetching
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-
-		this.limit = 30;
-		this.mode = 'all';
-
-		this.fetching = true;
-		this.moreFetching = false;
-
-		this.on('mount', () => {
-			this.fetch(() => this.$emit('loaded'));
-		});
-
-		this.fetch = cb => {
-			this.update({
-				fetching: true
-			});
-			this.opts.fetch(this.mode == 'iknow', this.limit, null, obj => {
-				this.update({
-					fetching: false,
-					users: obj.users,
-					next: obj.next
-				});
-				if (cb) cb();
-			});
-		};
-
-		this.more = () => {
-			this.update({
-				moreFetching: true
-			});
-			this.opts.fetch(this.mode == 'iknow', this.limit, this.next, obj => {
-				this.update({
-					moreFetching: false,
-					users: this.users.concat(obj.users),
-					next: obj.next
-				});
-			});
-		};
-
-		this.setMode = mode => {
-			this.update({
-				mode: mode
-			});
-			this.fetch();
-		};
-	</script>
-</mk-users-list>
diff --git a/src/web/app/mobile/views/components/user-followers.vue b/src/web/app/mobile/views/components/user-followers.vue
new file mode 100644
index 000000000..22629af9d
--- /dev/null
+++ b/src/web/app/mobile/views/components/user-followers.vue
@@ -0,0 +1,26 @@
+<template>
+<mk-users-list
+	:fetch="fetch"
+	:count="user.followers_count"
+	:you-know-count="user.followers_you_know_count"
+>
+	%i18n:mobile.tags.mk-user-followers.no-users%
+</mk-users-list>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	methods: {
+		fetch(iknow, limit, cursor, cb) {
+			this.$root.$data.os.api('users/followers', {
+				user_id: this.user.id,
+				iknow: iknow,
+				limit: limit,
+				cursor: cursor ? cursor : undefined
+			}).then(cb);
+		}
+	}
+});
+</script>
diff --git a/src/web/app/mobile/views/components/user-following.vue b/src/web/app/mobile/views/components/user-following.vue
new file mode 100644
index 000000000..bb739bc4c
--- /dev/null
+++ b/src/web/app/mobile/views/components/user-following.vue
@@ -0,0 +1,26 @@
+<template>
+<mk-users-list
+	:fetch="fetch"
+	:count="user.following_count"
+	:you-know-count="user.following_you_know_count"
+>
+	%i18n:mobile.tags.mk-user-following.no-users%
+</mk-users-list>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	methods: {
+		fetch(iknow, limit, cursor, cb) {
+			this.$root.$data.os.api('users/following', {
+				user_id: this.user.id,
+				iknow: iknow,
+				limit: limit,
+				cursor: cursor ? cursor : undefined
+			}).then(cb);
+		}
+	}
+});
+</script>
diff --git a/src/web/app/mobile/views/components/users-list.vue b/src/web/app/mobile/views/components/users-list.vue
new file mode 100644
index 000000000..54af40ec4
--- /dev/null
+++ b/src/web/app/mobile/views/components/users-list.vue
@@ -0,0 +1,126 @@
+<template>
+<div class="mk-users-list">
+	<nav>
+		<span :data-is-active="mode == 'all'" @click="mode = 'all'">%i18n:mobile.tags.mk-users-list.all%<span>{{ count }}</span></span>
+		<span v-if="$root.$data.os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:mobile.tags.mk-users-list.known%<span>{{ youKnowCount }}</span></span>
+	</nav>
+	<div class="users" v-if="!fetching && users.length != 0">
+		<mk-user-preview v-for="u in users" :user="u" :key="u.id"/>
+	</div>
+	<button class="more" v-if="!fetching && next != null" @click="more" :disabled="moreFetching">
+		<span v-if="!moreFetching">%i18n:mobile.tags.mk-users-list.load-more%</span>
+		<span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
+	</button>
+	<p class="no" v-if="!fetching && users.length == 0">
+		<slot></slot>
+	</p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['fetch', 'count', 'youKnowCount'],
+	data() {
+		return {
+			limit: 30,
+			mode: 'all',
+			fetching: true,
+			moreFetching: false,
+			users: [],
+			next: null
+		};
+	},
+	mounted() {
+		this._fetch(() => {
+			this.$emit('loaded');
+		});
+	},
+	methods: {
+		_fetch(cb) {
+			this.fetching = true;
+			this.fetch(this.mode == 'iknow', this.limit, null, obj => {
+				this.fetching = false;
+				this.users = obj.users;
+				this.next = obj.next;
+				if (cb) cb();
+			});
+		},
+		more() {
+			this.moreFetching = true;
+			this.fetch(this.mode == 'iknow', this.limit, this.next, obj => {
+				this.moreFetching = false;
+				this.users = this.users.concat(obj.users);
+				this.next = obj.next;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-users-list
+
+	> nav
+		display flex
+		justify-content center
+		margin 0 auto
+		max-width 600px
+		border-bottom solid 1px rgba(0, 0, 0, 0.2)
+
+		> span
+			display block
+			flex 1 1
+			text-align center
+			line-height 52px
+			font-size 14px
+			color #657786
+			border-bottom solid 2px transparent
+
+			&[data-is-active]
+				font-weight bold
+				color $theme-color
+				border-color $theme-color
+
+			> span
+				display inline-block
+				margin-left 4px
+				padding 2px 5px
+				font-size 12px
+				line-height 1
+				color #fff
+				background rgba(0, 0, 0, 0.3)
+				border-radius 20px
+
+	> .users
+		margin 8px auto
+		max-width 500px
+		width calc(100% - 16px)
+		background #fff
+		border-radius 8px
+		box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+
+		@media (min-width 500px)
+			margin 16px auto
+			width calc(100% - 32px)
+
+		> *
+			border-bottom solid 1px rgba(0, 0, 0, 0.05)
+
+	> .no
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+	> .fetching
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+</style>

From 0bf007df9e1cfdbea11ef07571eaa9d700ec19e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 13:08:06 +0900
Subject: [PATCH 147/286] wip

---
 src/web/app/desktop/-tags/user-followers.tag  |  23 ---
 src/web/app/desktop/-tags/user-following.tag  |  23 ---
 src/web/app/desktop/-tags/users-list.tag      | 138 ------------------
 .../views/components/user-followers.vue       |  26 ++++
 .../views/components/user-following.vue       |  26 ++++
 .../desktop/views/components/users-list.vue   | 136 +++++++++++++++++
 6 files changed, 188 insertions(+), 184 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/user-followers.tag
 delete mode 100644 src/web/app/desktop/-tags/user-following.tag
 delete mode 100644 src/web/app/desktop/-tags/users-list.tag
 create mode 100644 src/web/app/desktop/views/components/user-followers.vue
 create mode 100644 src/web/app/desktop/views/components/user-following.vue
 create mode 100644 src/web/app/desktop/views/components/users-list.vue

diff --git a/src/web/app/desktop/-tags/user-followers.tag b/src/web/app/desktop/-tags/user-followers.tag
deleted file mode 100644
index 3a5430d37..000000000
--- a/src/web/app/desktop/-tags/user-followers.tag
+++ /dev/null
@@ -1,23 +0,0 @@
-<mk-user-followers>
-	<mk-users-list fetch={ fetch } count={ user.followers_count } you-know-count={ user.followers_you_know_count } no-users={ 'フォロワーはいないようです。' }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			height 100%
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.user = this.opts.user;
-
-		this.fetch = (iknow, limit, cursor, cb) => {
-			this.$root.$data.os.api('users/followers', {
-				user_id: this.user.id,
-				iknow: iknow,
-				limit: limit,
-				cursor: cursor ? cursor : undefined
-			}).then(cb);
-		};
-	</script>
-</mk-user-followers>
diff --git a/src/web/app/desktop/-tags/user-following.tag b/src/web/app/desktop/-tags/user-following.tag
deleted file mode 100644
index 42ad5f88a..000000000
--- a/src/web/app/desktop/-tags/user-following.tag
+++ /dev/null
@@ -1,23 +0,0 @@
-<mk-user-following>
-	<mk-users-list fetch={ fetch } count={ user.following_count } you-know-count={ user.following_you_know_count } no-users={ 'フォロー中のユーザーはいないようです。' }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			height 100%
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.user = this.opts.user;
-
-		this.fetch = (iknow, limit, cursor, cb) => {
-			this.$root.$data.os.api('users/following', {
-				user_id: this.user.id,
-				iknow: iknow,
-				limit: limit,
-				cursor: cursor ? cursor : undefined
-			}).then(cb);
-		};
-	</script>
-</mk-user-following>
diff --git a/src/web/app/desktop/-tags/users-list.tag b/src/web/app/desktop/-tags/users-list.tag
deleted file mode 100644
index 03c527109..000000000
--- a/src/web/app/desktop/-tags/users-list.tag
+++ /dev/null
@@ -1,138 +0,0 @@
-<mk-users-list>
-	<nav>
-		<div>
-			<span data-is-active={ mode == 'all' } @click="setMode.bind(this, 'all')">すべて<span>{ opts.count }</span></span>
-			<span v-if="$root.$data.os.isSignedIn && opts.youKnowCount" data-is-active={ mode == 'iknow' } @click="setMode.bind(this, 'iknow')">知り合い<span>{ opts.youKnowCount }</span></span>
-		</div>
-	</nav>
-	<div class="users" v-if="!fetching && users.length != 0">
-		<div each={ users }>
-			<mk-list-user user={ this }/>
-		</div>
-	</div>
-	<button class="more" v-if="!fetching && next != null" @click="more" disabled={ moreFetching }>
-		<span v-if="!moreFetching">もっと</span>
-		<span v-if="moreFetching">読み込み中<mk-ellipsis/></span>
-	</button>
-	<p class="no" v-if="!fetching && users.length == 0">{ opts.noUsers }</p>
-	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			height 100%
-			background #fff
-
-			> nav
-				z-index 1
-				box-shadow 0 1px 0 rgba(#000, 0.1)
-
-				> div
-					display flex
-					justify-content center
-					margin 0 auto
-					max-width 600px
-
-					> span
-						display block
-						flex 1 1
-						text-align center
-						line-height 52px
-						font-size 14px
-						color #657786
-						border-bottom solid 2px transparent
-						cursor pointer
-
-						*
-							pointer-events none
-
-						&[data-is-active]
-							font-weight bold
-							color $theme-color
-							border-color $theme-color
-							cursor default
-
-						> span
-							display inline-block
-							margin-left 4px
-							padding 2px 5px
-							font-size 12px
-							line-height 1
-							color #888
-							background #eee
-							border-radius 20px
-
-			> .users
-				height calc(100% - 54px)
-				overflow auto
-
-				> *
-					border-bottom solid 1px rgba(0, 0, 0, 0.05)
-
-					> *
-						max-width 600px
-						margin 0 auto
-
-			> .no
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-			> .fetching
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-
-		this.limit = 30;
-		this.mode = 'all';
-
-		this.fetching = true;
-		this.moreFetching = false;
-
-		this.on('mount', () => {
-			this.fetch(() => this.$emit('loaded'));
-		});
-
-		this.fetch = cb => {
-			this.update({
-				fetching: true
-			});
-			this.opts.fetch(this.mode == 'iknow', this.limit, null, obj => {
-				this.update({
-					fetching: false,
-					users: obj.users,
-					next: obj.next
-				});
-				if (cb) cb();
-			});
-		};
-
-		this.more = () => {
-			this.update({
-				moreFetching: true
-			});
-			this.opts.fetch(this.mode == 'iknow', this.limit, this.cursor, obj => {
-				this.update({
-					moreFetching: false,
-					users: this.users.concat(obj.users),
-					next: obj.next
-				});
-			});
-		};
-
-		this.setMode = mode => {
-			this.update({
-				mode: mode
-			});
-			this.fetch();
-		};
-	</script>
-</mk-users-list>
diff --git a/src/web/app/desktop/views/components/user-followers.vue b/src/web/app/desktop/views/components/user-followers.vue
new file mode 100644
index 000000000..67e694cf4
--- /dev/null
+++ b/src/web/app/desktop/views/components/user-followers.vue
@@ -0,0 +1,26 @@
+<template>
+<mk-users-list
+	:fetch="fetch"
+	:count="user.followers_count"
+	:you-know-count="user.followers_you_know_count"
+>
+	フォロワーはいないようです。
+</mk-users-list>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	methods: {
+		fetch(iknow, limit, cursor, cb) {
+			this.$root.$data.os.api('users/followers', {
+				user_id: this.user.id,
+				iknow: iknow,
+				limit: limit,
+				cursor: cursor ? cursor : undefined
+			}).then(cb);
+		}
+	}
+});
+</script>
diff --git a/src/web/app/desktop/views/components/user-following.vue b/src/web/app/desktop/views/components/user-following.vue
new file mode 100644
index 000000000..16cc3c42f
--- /dev/null
+++ b/src/web/app/desktop/views/components/user-following.vue
@@ -0,0 +1,26 @@
+<template>
+<mk-users-list
+	:fetch="fetch"
+	:count="user.following_count"
+	:you-know-count="user.following_you_know_count"
+>
+	フォロー中のユーザーはいないようです。
+</mk-users-list>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	methods: {
+		fetch(iknow, limit, cursor, cb) {
+			this.$root.$data.os.api('users/following', {
+				user_id: this.user.id,
+				iknow: iknow,
+				limit: limit,
+				cursor: cursor ? cursor : undefined
+			}).then(cb);
+		}
+	}
+});
+</script>
diff --git a/src/web/app/desktop/views/components/users-list.vue b/src/web/app/desktop/views/components/users-list.vue
new file mode 100644
index 000000000..268fac4ec
--- /dev/null
+++ b/src/web/app/desktop/views/components/users-list.vue
@@ -0,0 +1,136 @@
+<template>
+<div class="mk-users-list">
+	<nav>
+		<div>
+			<span :data-is-active="mode == 'all'" @click="mode = 'all'">すべて<span>{{ count }}</span></span>
+			<span v-if="$root.$data.os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span>
+		</div>
+	</nav>
+	<div class="users" v-if="!fetching && users.length != 0">
+		<div v-for="u in users" :key="u.id">
+			<mk-list-user :user="u"/>
+		</div>
+	</div>
+	<button class="more" v-if="!fetching && next != null" @click="more" :disabled="moreFetching">
+		<span v-if="!moreFetching">もっと</span>
+		<span v-if="moreFetching">読み込み中<mk-ellipsis/></span>
+	</button>
+	<p class="no" v-if="!fetching && users.length == 0">
+		<slot></slot>
+	</p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['fetch', 'count', 'youKnowCount'],
+	data() {
+		return {
+			limit: 30,
+			mode: 'all',
+			fetching: true,
+			moreFetching: false,
+			users: [],
+			next: null
+		};
+	},
+	mounted() {
+		this._fetch(() => {
+			this.$emit('loaded');
+		});
+	},
+	methods: {
+		_fetch(cb) {
+			this.fetching = true;
+			this.fetch(this.mode == 'iknow', this.limit, null, obj => {
+				this.fetching = false;
+				this.users = obj.users;
+				this.next = obj.next;
+				if (cb) cb();
+			});
+		},
+		more() {
+			this.moreFetching = true;
+			this.fetch(this.mode == 'iknow', this.limit, this.next, obj => {
+				this.moreFetching = false;
+				this.users = this.users.concat(obj.users);
+				this.next = obj.next;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-users-list
+	height 100%
+	background #fff
+
+	> nav
+		z-index 1
+		box-shadow 0 1px 0 rgba(#000, 0.1)
+
+		> div
+			display flex
+			justify-content center
+			margin 0 auto
+			max-width 600px
+
+			> span
+				display block
+				flex 1 1
+				text-align center
+				line-height 52px
+				font-size 14px
+				color #657786
+				border-bottom solid 2px transparent
+				cursor pointer
+
+				*
+					pointer-events none
+
+				&[data-is-active]
+					font-weight bold
+					color $theme-color
+					border-color $theme-color
+					cursor default
+
+				> span
+					display inline-block
+					margin-left 4px
+					padding 2px 5px
+					font-size 12px
+					line-height 1
+					color #888
+					background #eee
+					border-radius 20px
+
+	> .users
+		height calc(100% - 54px)
+		overflow auto
+
+		> *
+			border-bottom solid 1px rgba(0, 0, 0, 0.05)
+
+			> *
+				max-width 600px
+				margin 0 auto
+
+	> .no
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+	> .fetching
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+</style>

From ec63c3463ad7a7eb5fdaa92519c754a1f5408d5d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 13:11:54 +0900
Subject: [PATCH 148/286] wip

---
 src/web/app/mobile/tags/page/user.tag   | 27 -------------------------
 src/web/app/mobile/views/pages/user.vue | 10 +++++++--
 2 files changed, 8 insertions(+), 29 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/page/user.tag

diff --git a/src/web/app/mobile/tags/page/user.tag b/src/web/app/mobile/tags/page/user.tag
deleted file mode 100644
index 3af11bbb4..000000000
--- a/src/web/app/mobile/tags/page/user.tag
+++ /dev/null
@@ -1,27 +0,0 @@
-<mk-user-page>
-	<mk-ui ref="ui">
-		<mk-user ref="user" user={ parent.user } page={ parent.opts.page }/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import ui from '../../scripts/ui-event';
-		import Progress from '../../../common/scripts/loading';
-
-		this.user = this.opts.user;
-
-		this.on('mount', () => {
-			document.documentElement.style.background = '#313a42';
-			Progress.start();
-
-			this.$refs.ui.refs.user.on('loaded', user => {
-				Progress.done();
-				document.title = user.name + ' | Misskey';
-				// TODO: ユーザー名をエスケープ
-				ui.trigger('title', '%fa:user%' + user.name);
-			});
-		});
-	</script>
-</mk-user-page>
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index d92f3bbe6..4cc152c1e 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-ui :func="fn" func-icon="%fa:pencil-alt%">
-	<span slot="header">%fa:user% {{user.name}}</span>
+	<span slot="header" v-if="!fetching">%fa:user% {{user.name}}</span>
 	<div v-if="!fetching" :class="$style.user">
 		<header>
 			<div class="banner" :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=1024)` : ''"></div>
@@ -58,6 +58,7 @@
 <script lang="ts">
 import Vue from 'vue';
 const age = require('s-age');
+import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
 	props: {
@@ -81,12 +82,17 @@ export default Vue.extend({
 		}
 	},
 	mounted() {
+		document.documentElement.style.background = '#313a42';
+		Progress.start();
+
 		this.$root.$data.os.api('users/show', {
 			username: this.username
 		}).then(user => {
 			this.fetching = false;
 			this.user = user;
-			this.$emit('loaded', user);
+
+			Progress.done();
+			document.title = user.name + ' | Misskey';
 		});
 	}
 });

From 131bbce66ee36fb4ed6773e977d2f7a4816a10de Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 13:19:23 +0900
Subject: [PATCH 149/286] wip

---
 src/web/app/mobile/tags/page/new-post.tag     |  7 ----
 .../app/mobile/tags/page/user-followers.tag   | 40 ------------------
 .../app/mobile/tags/page/user-following.tag   | 40 ------------------
 src/web/app/mobile/views/pages/followers.vue  | 42 +++++++++++++++++++
 src/web/app/mobile/views/pages/following.vue  | 42 +++++++++++++++++++
 5 files changed, 84 insertions(+), 87 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/page/new-post.tag
 delete mode 100644 src/web/app/mobile/tags/page/user-followers.tag
 delete mode 100644 src/web/app/mobile/tags/page/user-following.tag
 create mode 100644 src/web/app/mobile/views/pages/followers.vue
 create mode 100644 src/web/app/mobile/views/pages/following.vue

diff --git a/src/web/app/mobile/tags/page/new-post.tag b/src/web/app/mobile/tags/page/new-post.tag
deleted file mode 100644
index 1650446b4..000000000
--- a/src/web/app/mobile/tags/page/new-post.tag
+++ /dev/null
@@ -1,7 +0,0 @@
-<mk-new-post-page>
-	<mk-post-form ref="form"/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-</mk-new-post-page>
diff --git a/src/web/app/mobile/tags/page/user-followers.tag b/src/web/app/mobile/tags/page/user-followers.tag
deleted file mode 100644
index a65809484..000000000
--- a/src/web/app/mobile/tags/page/user-followers.tag
+++ /dev/null
@@ -1,40 +0,0 @@
-<mk-user-followers-page>
-	<mk-ui ref="ui">
-		<mk-user-followers ref="list" v-if="!parent.fetching" user={ parent.user }/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import ui from '../../scripts/ui-event';
-		import Progress from '../../../common/scripts/loading';
-
-		this.mixin('api');
-
-		this.fetching = true;
-		this.user = null;
-
-		this.on('mount', () => {
-			Progress.start();
-
-			this.$root.$data.os.api('users/show', {
-				username: this.opts.user
-			}).then(user => {
-				this.update({
-					fetching: false,
-					user: user
-				});
-
-				document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
-				// TODO: ユーザー名をエスケープ
-				ui.trigger('title', '<img src="' + user.avatar_url + '?thumbnail&size=64">' +  '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name));
-				document.documentElement.style.background = '#313a42';
-
-				this.$refs.ui.refs.list.on('loaded', () => {
-					Progress.done();
-				});
-			});
-		});
-	</script>
-</mk-user-followers-page>
diff --git a/src/web/app/mobile/tags/page/user-following.tag b/src/web/app/mobile/tags/page/user-following.tag
deleted file mode 100644
index 8fe0f5fce..000000000
--- a/src/web/app/mobile/tags/page/user-following.tag
+++ /dev/null
@@ -1,40 +0,0 @@
-<mk-user-following-page>
-	<mk-ui ref="ui">
-		<mk-user-following ref="list" v-if="!parent.fetching" user={ parent.user }/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import ui from '../../scripts/ui-event';
-		import Progress from '../../../common/scripts/loading';
-
-		this.mixin('api');
-
-		this.fetching = true;
-		this.user = null;
-
-		this.on('mount', () => {
-			Progress.start();
-
-			this.$root.$data.os.api('users/show', {
-				username: this.opts.user
-			}).then(user => {
-				this.update({
-					fetching: false,
-					user: user
-				});
-
-				document.title = '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', user.name) + ' | Misskey';
-				// TODO: ユーザー名をエスケープ
-				ui.trigger('title', '<img src="' + user.avatar_url + '?thumbnail&size=64">' + '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', user.name));
-				document.documentElement.style.background = '#313a42';
-
-				this.$refs.ui.refs.list.on('loaded', () => {
-					Progress.done();
-				});
-			});
-		});
-	</script>
-</mk-user-following-page>
diff --git a/src/web/app/mobile/views/pages/followers.vue b/src/web/app/mobile/views/pages/followers.vue
new file mode 100644
index 000000000..dcaca16a2
--- /dev/null
+++ b/src/web/app/mobile/views/pages/followers.vue
@@ -0,0 +1,42 @@
+<template>
+<mk-ui>
+	<span slot="header" v-if="!fetching">
+		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt="">
+		{{ '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) }}
+	</span>
+	<mk-user-followers v-if="!fetching" :user="user" @loaded="onLoaded"/>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+	props: ['username'],
+	data() {
+		return {
+			fetching: true,
+			user: null
+		};
+	},
+	mounted() {
+		Progress.start();
+
+		this.$root.$data.os.api('users/show', {
+			username: this.username
+		}).then(user => {
+			this.fetching = false;
+			this.user = user;
+
+			document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
+			document.documentElement.style.background = '#313a42';
+		});
+	},
+	methods: {
+		onLoaded() {
+			Progress.done();
+		}
+	}
+});
+</script>
diff --git a/src/web/app/mobile/views/pages/following.vue b/src/web/app/mobile/views/pages/following.vue
new file mode 100644
index 000000000..b11e3b95f
--- /dev/null
+++ b/src/web/app/mobile/views/pages/following.vue
@@ -0,0 +1,42 @@
+<template>
+<mk-ui>
+	<span slot="header" v-if="!fetching">
+		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt="">
+		{{ '%i18n:mobile.tags.mk-user-following-page.following-of'.replace('{}', user.name) }}
+	</span>
+	<mk-user-following v-if="!fetching" :user="user" @loaded="onLoaded"/>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+	props: ['username'],
+	data() {
+		return {
+			fetching: true,
+			user: null
+		};
+	},
+	mounted() {
+		Progress.start();
+
+		this.$root.$data.os.api('users/show', {
+			username: this.username
+		}).then(user => {
+			this.fetching = false;
+			this.user = user;
+
+			document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
+			document.documentElement.style.background = '#313a42';
+		});
+	},
+	methods: {
+		onLoaded() {
+			Progress.done();
+		}
+	}
+});
+</script>

From 02f3d8b0e07e199872af02ffd5e3e828d8ed6dc0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 14:04:18 +0900
Subject: [PATCH 150/286] wip

---
 src/web/app/desktop/-tags/pages/drive.tag | 37 -------------------
 src/web/app/desktop/views/pages/drive.vue | 45 +++++++++++++++++++++++
 2 files changed, 45 insertions(+), 37 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/pages/drive.tag
 create mode 100644 src/web/app/desktop/views/pages/drive.vue

diff --git a/src/web/app/desktop/-tags/pages/drive.tag b/src/web/app/desktop/-tags/pages/drive.tag
deleted file mode 100644
index f4e2a3740..000000000
--- a/src/web/app/desktop/-tags/pages/drive.tag
+++ /dev/null
@@ -1,37 +0,0 @@
-<mk-drive-page>
-	<mk-drive-browser ref="browser" folder={ opts.folder }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			position fixed
-			width 100%
-			height 100%
-			background #fff
-
-			> mk-drive-browser
-				height 100%
-	</style>
-	<script lang="typescript">
-		this.on('mount', () => {
-			document.title = 'Misskey Drive';
-
-			this.$refs.browser.on('move-root', () => {
-				const title = 'Misskey Drive';
-
-				// Rewrite URL
-				history.pushState(null, title, '/i/drive');
-
-				document.title = title;
-			});
-
-			this.$refs.browser.on('open-folder', folder => {
-				const title = folder.name + ' | Misskey Drive';
-
-				// Rewrite URL
-				history.pushState(null, title, '/i/drive/folder/' + folder.id);
-
-				document.title = title;
-			});
-		});
-	</script>
-</mk-drive-page>
diff --git a/src/web/app/desktop/views/pages/drive.vue b/src/web/app/desktop/views/pages/drive.vue
new file mode 100644
index 000000000..3ce5af769
--- /dev/null
+++ b/src/web/app/desktop/views/pages/drive.vue
@@ -0,0 +1,45 @@
+<template>
+<div class="mk-drive-page">
+	<mk-drive :folder="folder" @move-root="onMoveRoot" @open-folder="onOpenFolder"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['folder'],
+	mounted() {
+		document.title = 'Misskey Drive';
+	},
+	methods: {
+		onMoveRoot() {
+			const title = 'Misskey Drive';
+
+			// Rewrite URL
+			history.pushState(null, title, '/i/drive');
+
+			document.title = title;
+		},
+		onOpenFolder(folder) {
+			const title = folder.name + ' | Misskey Drive';
+
+			// Rewrite URL
+			history.pushState(null, title, '/i/drive/folder/' + folder.id);
+
+			document.title = title;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-drive-page
+	position fixed
+	width 100%
+	height 100%
+	background #fff
+
+	> .mk-drive
+		height 100%
+</style>
+

From a16c23b6f6bcca4c2220541c055a6f48509f12e6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 14:22:34 +0900
Subject: [PATCH 151/286] wip

---
 .../app/desktop/-tags/pages/selectdrive.tag   | 161 ----------------
 .../app/desktop/views/pages/selectdrive.vue   | 175 ++++++++++++++++++
 2 files changed, 175 insertions(+), 161 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/pages/selectdrive.tag
 create mode 100644 src/web/app/desktop/views/pages/selectdrive.vue

diff --git a/src/web/app/desktop/-tags/pages/selectdrive.tag b/src/web/app/desktop/-tags/pages/selectdrive.tag
deleted file mode 100644
index dd4d30f41..000000000
--- a/src/web/app/desktop/-tags/pages/selectdrive.tag
+++ /dev/null
@@ -1,161 +0,0 @@
-<mk-selectdrive-page>
-	<mk-drive-browser ref="browser" multiple={ multiple }/>
-	<div>
-		<button class="upload" title="%i18n:desktop.tags.mk-selectdrive-page.upload%" @click="upload">%fa:upload%</button>
-		<button class="cancel" @click="close">%i18n:desktop.tags.mk-selectdrive-page.cancel%</button>
-		<button class="ok" @click="ok">%i18n:desktop.tags.mk-selectdrive-page.ok%</button>
-	</div>
-
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			position fixed
-			width 100%
-			height 100%
-			background #fff
-
-			> mk-drive-browser
-				height calc(100% - 72px)
-
-			> div
-				position fixed
-				bottom 0
-				left 0
-				width 100%
-				height 72px
-				background lighten($theme-color, 95%)
-
-				.upload
-					display inline-block
-					position absolute
-					top 8px
-					left 16px
-					cursor pointer
-					padding 0
-					margin 8px 4px 0 0
-					width 40px
-					height 40px
-					font-size 1em
-					color rgba($theme-color, 0.5)
-					background transparent
-					outline none
-					border solid 1px transparent
-					border-radius 4px
-
-					&:hover
-						background transparent
-						border-color rgba($theme-color, 0.3)
-
-					&:active
-						color rgba($theme-color, 0.6)
-						background transparent
-						border-color rgba($theme-color, 0.5)
-						box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset
-
-					&:focus
-						&:after
-							content ""
-							pointer-events none
-							position absolute
-							top -5px
-							right -5px
-							bottom -5px
-							left -5px
-							border 2px solid rgba($theme-color, 0.3)
-							border-radius 8px
-
-				.ok
-				.cancel
-					display block
-					position absolute
-					bottom 16px
-					cursor pointer
-					padding 0
-					margin 0
-					width 120px
-					height 40px
-					font-size 1em
-					outline none
-					border-radius 4px
-
-					&:focus
-						&:after
-							content ""
-							pointer-events none
-							position absolute
-							top -5px
-							right -5px
-							bottom -5px
-							left -5px
-							border 2px solid rgba($theme-color, 0.3)
-							border-radius 8px
-
-					&:disabled
-						opacity 0.7
-						cursor default
-
-				.ok
-					right 16px
-					color $theme-color-foreground
-					background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
-					border solid 1px lighten($theme-color, 15%)
-
-					&:not(:disabled)
-						font-weight bold
-
-					&:hover:not(:disabled)
-						background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
-						border-color $theme-color
-
-					&:active:not(:disabled)
-						background $theme-color
-						border-color $theme-color
-
-				.cancel
-					right 148px
-					color #888
-					background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
-					border solid 1px #e2e2e2
-
-					&:hover
-						background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
-						border-color #dcdcdc
-
-					&:active
-						background #ececec
-						border-color #dcdcdc
-
-	</style>
-	<script lang="typescript">
-		const q = (new URL(location)).searchParams;
-		this.multiple = q.get('multiple') == 'true' ? true : false;
-
-		this.on('mount', () => {
-			document.title = '%i18n:desktop.tags.mk-selectdrive-page.title%';
-
-			this.$refs.browser.on('selected', file => {
-				this.files = [file];
-				this.ok();
-			});
-
-			this.$refs.browser.on('change-selection', files => {
-				this.update({
-					files: files
-				});
-			});
-		});
-
-		this.upload = () => {
-			this.$refs.browser.selectLocalFile();
-		};
-
-		this.close = () => {
-			window.close();
-		};
-
-		this.ok = () => {
-			window.opener.cb(this.multiple ? this.files : this.files[0]);
-			window.close();
-		};
-	</script>
-</mk-selectdrive-page>
diff --git a/src/web/app/desktop/views/pages/selectdrive.vue b/src/web/app/desktop/views/pages/selectdrive.vue
new file mode 100644
index 000000000..da31ef8f0
--- /dev/null
+++ b/src/web/app/desktop/views/pages/selectdrive.vue
@@ -0,0 +1,175 @@
+<template>
+<div class="mk-selectdrive">
+	<mk-drive ref="browser"
+		:multiple="multiple"
+		@selected="onSelected"
+		@change-selection="onChangeSelection"
+	/>
+	<div>
+		<button class="upload" title="%i18n:desktop.tags.mk-selectdrive-page.upload%" @click="upload">%fa:upload%</button>
+		<button class="cancel" @click="close">%i18n:desktop.tags.mk-selectdrive-page.cancel%</button>
+		<button class="ok" @click="ok">%i18n:desktop.tags.mk-selectdrive-page.ok%</button>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	data() {
+		return {
+			files: []
+		};
+	},
+	computed: {
+		multiple(): boolean {
+			const q = (new URL(location.toString())).searchParams;
+			return q.get('multiple') == 'true';
+		}
+	},
+	mounted() {
+		document.title = '%i18n:desktop.tags.mk-selectdrive-page.title%';
+	},
+	methods: {
+		onSelected(file) {
+			this.files = [file];
+			this.ok();
+		},
+		onChangeSelection(files) {
+			this.files = files;
+		},
+		upload() {
+			(this.$refs.browser as any).selectLocalFile();
+		},
+		close() {
+			window.close();
+		},
+		ok() {
+			window.opener.cb(this.multiple ? this.files : this.files[0]);
+			this.close();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-selectdrive
+	display block
+	position fixed
+	width 100%
+	height 100%
+	background #fff
+
+	> .mk-drive
+		height calc(100% - 72px)
+
+	> div
+		position fixed
+		bottom 0
+		left 0
+		width 100%
+		height 72px
+		background lighten($theme-color, 95%)
+
+		.upload
+			display inline-block
+			position absolute
+			top 8px
+			left 16px
+			cursor pointer
+			padding 0
+			margin 8px 4px 0 0
+			width 40px
+			height 40px
+			font-size 1em
+			color rgba($theme-color, 0.5)
+			background transparent
+			outline none
+			border solid 1px transparent
+			border-radius 4px
+
+			&:hover
+				background transparent
+				border-color rgba($theme-color, 0.3)
+
+			&:active
+				color rgba($theme-color, 0.6)
+				background transparent
+				border-color rgba($theme-color, 0.5)
+				box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset
+
+			&:focus
+				&:after
+					content ""
+					pointer-events none
+					position absolute
+					top -5px
+					right -5px
+					bottom -5px
+					left -5px
+					border 2px solid rgba($theme-color, 0.3)
+					border-radius 8px
+
+		.ok
+		.cancel
+			display block
+			position absolute
+			bottom 16px
+			cursor pointer
+			padding 0
+			margin 0
+			width 120px
+			height 40px
+			font-size 1em
+			outline none
+			border-radius 4px
+
+			&:focus
+				&:after
+					content ""
+					pointer-events none
+					position absolute
+					top -5px
+					right -5px
+					bottom -5px
+					left -5px
+					border 2px solid rgba($theme-color, 0.3)
+					border-radius 8px
+
+			&:disabled
+				opacity 0.7
+				cursor default
+
+		.ok
+			right 16px
+			color $theme-color-foreground
+			background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+			border solid 1px lighten($theme-color, 15%)
+
+			&:not(:disabled)
+				font-weight bold
+
+			&:hover:not(:disabled)
+				background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
+				border-color $theme-color
+
+			&:active:not(:disabled)
+				background $theme-color
+				border-color $theme-color
+
+		.cancel
+			right 148px
+			color #888
+			background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
+			border solid 1px #e2e2e2
+
+			&:hover
+				background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
+				border-color #dcdcdc
+
+			&:active
+				background #ececec
+				border-color #dcdcdc
+
+</style>

From aa433c2ac9ad5a3f86fbfbb4f4ef9fee3657257d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 14:25:33 +0900
Subject: [PATCH 152/286] wip

---
 src/web/app/mobile/tags/page/selectdrive.tag  | 87 -----------------
 .../app/mobile/views/pages/selectdrive.vue    | 96 +++++++++++++++++++
 2 files changed, 96 insertions(+), 87 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/page/selectdrive.tag
 create mode 100644 src/web/app/mobile/views/pages/selectdrive.vue

diff --git a/src/web/app/mobile/tags/page/selectdrive.tag b/src/web/app/mobile/tags/page/selectdrive.tag
deleted file mode 100644
index b410d4603..000000000
--- a/src/web/app/mobile/tags/page/selectdrive.tag
+++ /dev/null
@@ -1,87 +0,0 @@
-<mk-selectdrive-page>
-	<header>
-		<h1>%i18n:mobile.tags.mk-selectdrive-page.select-file%<span class="count" v-if="files.length > 0">({ files.length })</span></h1>
-		<button class="upload" @click="upload">%fa:upload%</button>
-		<button v-if="multiple" class="ok" @click="ok">%fa:check%</button>
-	</header>
-	<mk-drive ref="browser" select-file={ true } multiple={ multiple } is-naked={ true } top={ 42 }/>
-
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			width 100%
-			height 100%
-			background #fff
-
-			> header
-				position fixed
-				top 0
-				left 0
-				width 100%
-				z-index 1000
-				background #fff
-				box-shadow 0 1px rgba(0, 0, 0, 0.1)
-
-				> h1
-					margin 0
-					padding 0
-					text-align center
-					line-height 42px
-					font-size 1em
-					font-weight normal
-
-					> .count
-						margin-left 4px
-						opacity 0.5
-
-				> .upload
-					position absolute
-					top 0
-					left 0
-					line-height 42px
-					width 42px
-
-				> .ok
-					position absolute
-					top 0
-					right 0
-					line-height 42px
-					width 42px
-
-			> mk-drive
-				top 42px
-
-	</style>
-	<script lang="typescript">
-		const q = (new URL(location)).searchParams;
-		this.multiple = q.get('multiple') == 'true' ? true : false;
-
-		this.on('mount', () => {
-			document.documentElement.style.background = '#fff';
-
-			this.$refs.browser.on('selected', file => {
-				this.files = [file];
-				this.ok();
-			});
-
-			this.$refs.browser.on('change-selection', files => {
-				this.update({
-					files: files
-				});
-			});
-		});
-
-		this.upload = () => {
-			this.$refs.browser.selectLocalFile();
-		};
-
-		this.close = () => {
-			window.close();
-		};
-
-		this.ok = () => {
-			window.opener.cb(this.multiple ? this.files : this.files[0]);
-			window.close();
-		};
-	</script>
-</mk-selectdrive-page>
diff --git a/src/web/app/mobile/views/pages/selectdrive.vue b/src/web/app/mobile/views/pages/selectdrive.vue
new file mode 100644
index 000000000..3480a0d10
--- /dev/null
+++ b/src/web/app/mobile/views/pages/selectdrive.vue
@@ -0,0 +1,96 @@
+<template>
+<div class="mk-selectdrive">
+	<header>
+		<h1>%i18n:mobile.tags.mk-selectdrive-page.select-file%<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1>
+		<button class="upload" @click="upload">%fa:upload%</button>
+		<button v-if="multiple" class="ok" @click="ok">%fa:check%</button>
+	</header>
+	<mk-drive ref="browser" select-file :multiple="multiple" is-naked :top="42"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	data() {
+		return {
+			files: []
+		};
+	},
+	computed: {
+		multiple(): boolean {
+			const q = (new URL(location.toString())).searchParams;
+			return q.get('multiple') == 'true';
+		}
+	},
+	mounted() {
+		document.title = '%i18n:desktop.tags.mk-selectdrive-page.title%';
+	},
+	methods: {
+		onSelected(file) {
+			this.files = [file];
+			this.ok();
+		},
+		onChangeSelection(files) {
+			this.files = files;
+		},
+		upload() {
+			(this.$refs.browser as any).selectLocalFile();
+		},
+		close() {
+			window.close();
+		},
+		ok() {
+			window.opener.cb(this.multiple ? this.files : this.files[0]);
+			this.close();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-selectdrive
+	width 100%
+	height 100%
+	background #fff
+
+	> header
+		position fixed
+		top 0
+		left 0
+		width 100%
+		z-index 1000
+		background #fff
+		box-shadow 0 1px rgba(0, 0, 0, 0.1)
+
+		> h1
+			margin 0
+			padding 0
+			text-align center
+			line-height 42px
+			font-size 1em
+			font-weight normal
+
+			> .count
+				margin-left 4px
+				opacity 0.5
+
+		> .upload
+			position absolute
+			top 0
+			left 0
+			line-height 42px
+			width 42px
+
+		> .ok
+			position absolute
+			top 0
+			right 0
+			line-height 42px
+			width 42px
+
+	> .mk-drive
+		top 42px
+
+</style>

From 9b8e31a03576ac5de88efff6f5c4f18b5c9e86b3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 15:38:12 +0900
Subject: [PATCH 153/286] wip

---
 src/web/app/common/views/components/index.ts  |  4 ++
 .../views/components/reactions-viewer.vue     | 50 ++++++++---------
 .../app/common/views/components/uploader.vue  | 11 ++--
 .../views/components/post-form-window.vue     |  4 +-
 .../desktop/views/components/post-form.vue    | 54 ++++++++++---------
 .../app/desktop/views/components/window.vue   |  1 +
 6 files changed, 67 insertions(+), 57 deletions(-)

diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 48e9e9db0..452621756 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -6,8 +6,10 @@ import forkit from './forkit.vue';
 import nav from './nav.vue';
 import postHtml from './post-html';
 import reactionIcon from './reaction-icon.vue';
+import reactionsViewer from './reactions-viewer.vue';
 import time from './time.vue';
 import images from './images.vue';
+import uploader from './uploader.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
@@ -15,5 +17,7 @@ Vue.component('mk-forkit', forkit);
 Vue.component('mk-nav', nav);
 Vue.component('mk-post-html', postHtml);
 Vue.component('mk-reaction-icon', reactionIcon);
+Vue.component('mk-reactions-viewer', reactionsViewer);
 Vue.component('mk-time', time);
 Vue.component('mk-images', images);
+Vue.component('mk-uploader', uploader);
diff --git a/src/web/app/common/views/components/reactions-viewer.vue b/src/web/app/common/views/components/reactions-viewer.vue
index f6e37caa4..696aef335 100644
--- a/src/web/app/common/views/components/reactions-viewer.vue
+++ b/src/web/app/common/views/components/reactions-viewer.vue
@@ -1,5 +1,5 @@
 <template>
-<div>
+<div class="mk-reactions-viewer">
 	<template v-if="reactions">
 		<span v-if="reactions.like"><mk-reaction-icon reaction='like'/><span>{{ reactions.like }}</span></span>
 		<span v-if="reactions.love"><mk-reaction-icon reaction='love'/><span>{{ reactions.love }}</span></span>
@@ -14,36 +14,36 @@
 </div>
 </template>
 
-<script lang="typescript">
-	export default {
-		props: ['post'],
-		computed: {
-			reactions() {
-				return this.post.reaction_counts;
-			}
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['post'],
+	computed: {
+		reactions(): number {
+			return this.post.reaction_counts;
 		}
-	};
+	}
+});
 </script>
 
 <style lang="stylus" scoped>
-	:scope
-		display block
-		border-top dashed 1px #eee
-		border-bottom dashed 1px #eee
-		margin 4px 0
+.mk-reactions-viewer
+	border-top dashed 1px #eee
+	border-bottom dashed 1px #eee
+	margin 4px 0
 
-		&:empty
-			display none
+	&:empty
+		display none
+
+	> span
+		margin-right 8px
+
+		> mk-reaction-icon
+			font-size 1.4em
 
 		> span
-			margin-right 8px
-
-			> mk-reaction-icon
-				font-size 1.4em
-
-			> span
-				margin-left 4px
-				font-size 1.2em
-				color #444
+			margin-left 4px
+			font-size 1.2em
+			color #444
 
 </style>
diff --git a/src/web/app/common/views/components/uploader.vue b/src/web/app/common/views/components/uploader.vue
index 740d03ea5..21f92caab 100644
--- a/src/web/app/common/views/components/uploader.vue
+++ b/src/web/app/common/views/components/uploader.vue
@@ -19,6 +19,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import { apiUrl } from '../../../config';
+
 export default Vue.extend({
 	data() {
 		return {
@@ -34,14 +36,15 @@ export default Vue.extend({
 			const ctx = {
 				id: id,
 				name: file.name || 'untitled',
-				progress: undefined
+				progress: undefined,
+				img: undefined
 			};
 
 			this.uploads.push(ctx);
 			this.$emit('change', this.uploads);
 
 			const reader = new FileReader();
-			reader.onload = e => {
+			reader.onload = (e: any) => {
 				ctx.img = e.target.result;
 			};
 			reader.readAsDataURL(file);
@@ -53,8 +56,8 @@ export default Vue.extend({
 			if (folder) data.append('folder_id', folder);
 
 			const xhr = new XMLHttpRequest();
-			xhr.open('POST', _API_URL_ + '/drive/files/create', true);
-			xhr.onload = e => {
+			xhr.open('POST', apiUrl + '/drive/files/create', true);
+			xhr.onload = (e: any) => {
 				const driveFile = JSON.parse(e.target.response);
 
 				this.$emit('uploaded', driveFile);
diff --git a/src/web/app/desktop/views/components/post-form-window.vue b/src/web/app/desktop/views/components/post-form-window.vue
index 77b47e20a..127233370 100644
--- a/src/web/app/desktop/views/components/post-form-window.vue
+++ b/src/web/app/desktop/views/components/post-form-window.vue
@@ -34,8 +34,8 @@ export default Vue.extend({
 		});
 	},
 	methods: {
-		onChangeUploadings(media) {
-			this.uploadings = media;
+		onChangeUploadings(files) {
+			this.uploadings = files;
 		},
 		onChangeMedia(media) {
 			this.media = media;
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 0a5f8812d..502851316 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -22,12 +22,12 @@
 		<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false"/>
 	</div>
 	<mk-uploader @uploaded="attachMedia" @change="onChangeUploadings"/>
-	<button ref="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" @click="selectFile">%fa:upload%</button>
-	<button ref="drive" title="%i18n:desktop.tags.mk-post-form.attach-media-from-drive%" @click="selectFileFromDrive">%fa:cloud%</button>
+	<button class="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" @click="chooseFile">%fa:upload%</button>
+	<button class="drive" title="%i18n:desktop.tags.mk-post-form.attach-media-from-drive%" @click="chooseFileFromDrive">%fa:cloud%</button>
 	<button class="kao" title="%i18n:desktop.tags.mk-post-form.insert-a-kao%" @click="kao">%fa:R smile%</button>
 	<button class="poll" title="%i18n:desktop.tags.mk-post-form.create-poll%" @click="poll = true">%fa:chart-pie%</button>
 	<p class="text-count" :class="{ over: text.length > 1000 }">{{ '%i18n:desktop.tags.mk-post-form.text-remain%'.replace('{}', 1000 - text.length) }}</p>
-	<button :class="{ posting }" ref="submit" :disabled="!canPost" @click="post">
+	<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
 		{{ posting ? '%i18n:desktop.tags.mk-post-form.posting%' : submitText }}<mk-ellipsis v-if="posting"/>
 	</button>
 	<input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
@@ -82,23 +82,25 @@ export default Vue.extend({
 		}
 	},
 	mounted() {
-		this.autocomplete = new Autocomplete(this.$refs.text);
-		this.autocomplete.attach();
+		Vue.nextTick(() => {
+			this.autocomplete = new Autocomplete(this.$refs.text);
+			this.autocomplete.attach();
 
-		// 書きかけの投稿を復元
-		const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
-		if (draft) {
-			this.text = draft.data.text;
-			this.files = draft.data.files;
-			if (draft.data.poll) {
-				this.poll = true;
-				(this.$refs.poll as any).set(draft.data.poll);
+			// 書きかけの投稿を復元
+			const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
+			if (draft) {
+				this.text = draft.data.text;
+				this.files = draft.data.files;
+				if (draft.data.poll) {
+					this.poll = true;
+					(this.$refs.poll as any).set(draft.data.poll);
+				}
+				this.$emit('change-attached-media', this.files);
 			}
-			this.$emit('change-attached-media', this.files);
-		}
 
-		new Sortable(this.$refs.media, {
-			animation: 150
+			new Sortable(this.$refs.media, {
+				animation: 150
+			});
 		});
 	},
 	beforeDestroy() {
@@ -145,7 +147,7 @@ export default Vue.extend({
 			this.text = '';
 			this.files = [];
 			this.poll = false;
-			this.$emit('change-attached-media');
+			this.$emit('change-attached-media', this.files);
 		},
 		onKeydown(e) {
 			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
@@ -187,7 +189,7 @@ export default Vue.extend({
 				// (ドライブの)ファイルだったら
 				if (obj.type == 'file') {
 					this.files.push(obj.file);
-					this.$emit('change-attached-media');
+					this.$emit('change-attached-media', this.files);
 				}
 			} catch (e) { }
 		},
@@ -260,7 +262,7 @@ export default Vue.extend({
 
 	> .content
 
-		[ref='text']
+		textarea
 			display block
 			padding 12px
 			margin 0
@@ -364,20 +366,20 @@ export default Vue.extend({
 						height 16px
 						cursor pointer
 
-		> mk-poll-editor
+		> .mk-poll-editor
 			background lighten($theme-color, 98%)
 			border solid 1px rgba($theme-color, 0.1)
 			border-top none
 			border-radius 0 0 4px 4px
 			transition border-color .3s ease
 
-	> mk-uploader
+	> .mk-uploader
 		margin 8px 0 0 0
 		padding 8px
 		border solid 1px rgba($theme-color, 0.2)
 		border-radius 4px
 
-	[ref='file']
+	input[type='file']
 		display none
 
 	.text-count
@@ -393,7 +395,7 @@ export default Vue.extend({
 		&.over
 			color #ec3828
 
-	[ref='submit']
+	.submit
 		display block
 		position absolute
 		bottom 16px
@@ -457,8 +459,8 @@ export default Vue.extend({
 				from {background-position: 0 0;}
 				to   {background-position: -64px 32px;}
 
-	[ref='upload']
-	[ref='drive']
+	.upload
+	.drive
 	.kao
 	.poll
 		display inline-block
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 414858a1e..069d4c4f9 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -162,6 +162,7 @@ export default Vue.extend({
 			});
 
 			setTimeout(() => {
+				this.$destroy();
 				this.$emit('closed');
 			}, 300);
 		},

From d19897c8c763ad26b15743796e99215b5710ebd4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 15:52:28 +0900
Subject: [PATCH 154/286] wip

---
 .../app/mobile/tags/page/notifications.tag    | 39 -------------------
 .../app/mobile/views/pages/notification.vue   | 31 +++++++++++++++
 2 files changed, 31 insertions(+), 39 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/page/notifications.tag
 create mode 100644 src/web/app/mobile/views/pages/notification.vue

diff --git a/src/web/app/mobile/tags/page/notifications.tag b/src/web/app/mobile/tags/page/notifications.tag
deleted file mode 100644
index 169ff029b..000000000
--- a/src/web/app/mobile/tags/page/notifications.tag
+++ /dev/null
@@ -1,39 +0,0 @@
-<mk-notifications-page>
-	<mk-ui ref="ui">
-		<mk-notifications ref="notifications"/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import ui from '../../scripts/ui-event';
-		import Progress from '../../../common/scripts/loading';
-
-		this.mixin('api');
-
-		this.on('mount', () => {
-			document.title = 'Misskey | %i18n:mobile.tags.mk-notifications-page.notifications%';
-			ui.trigger('title', '%fa:R bell%%i18n:mobile.tags.mk-notifications-page.notifications%');
-			document.documentElement.style.background = '#313a42';
-
-			ui.trigger('func', () => {
-				this.readAll();
-			}, '%fa:check%');
-
-			Progress.start();
-
-			this.$refs.ui.refs.notifications.on('fetched', () => {
-				Progress.done();
-			});
-		});
-
-		this.readAll = () => {
-			const ok = window.confirm('%i18n:mobile.tags.mk-notifications-page.read-all%');
-
-			if (!ok) return;
-
-			this.$root.$data.os.api('notifications/mark_as_read_all');
-		};
-	</script>
-</mk-notifications-page>
diff --git a/src/web/app/mobile/views/pages/notification.vue b/src/web/app/mobile/views/pages/notification.vue
new file mode 100644
index 000000000..03d8b6cad
--- /dev/null
+++ b/src/web/app/mobile/views/pages/notification.vue
@@ -0,0 +1,31 @@
+<template>
+<mk-ui :func="fn" func-icon="%fa:check%">
+	<span slot="header">%fa:R bell%%i18n:mobile.tags.mk-notifications-page.notifications%</span>
+	<mk-notifications @fetched="onFetched"/>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+	mounted() {
+		document.title = 'Misskey | %i18n:mobile.tags.mk-notifications-page.notifications%';
+		document.documentElement.style.background = '#313a42';
+
+		Progress.start();
+	},
+	methods: {
+		fn() {
+			const ok = window.confirm('%i18n:mobile.tags.mk-notifications-page.read-all%');
+			if (!ok) return;
+
+			this.$root.$data.os.api('notifications/mark_as_read_all');
+		},
+		onFetched() {
+			Progress.done();
+		}
+	}
+});
+</script>

From f8580211ff9ccbd85678cc49b00863544a00fa21 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 16:51:02 +0900
Subject: [PATCH 155/286] wip

---
 src/web/app/desktop/-tags/user-preview.tag    | 149 ----------------
 .../desktop/views/components/user-preview.vue | 160 ++++++++++++++++++
 2 files changed, 160 insertions(+), 149 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/user-preview.tag
 create mode 100644 src/web/app/desktop/views/components/user-preview.vue

diff --git a/src/web/app/desktop/-tags/user-preview.tag b/src/web/app/desktop/-tags/user-preview.tag
deleted file mode 100644
index 8503e9aeb..000000000
--- a/src/web/app/desktop/-tags/user-preview.tag
+++ /dev/null
@@ -1,149 +0,0 @@
-<mk-user-preview>
-	<template v-if="user != null">
-		<div class="banner" style={ user.banner_url ? 'background-image: url(' + user.banner_url + '?thumbnail&size=512)' : '' }></div><a class="avatar" href={ '/' + user.username } target="_blank"><img src={ user.avatar_url + '?thumbnail&size=64' } alt="avatar"/></a>
-		<div class="title">
-			<p class="name">{ user.name }</p>
-			<p class="username">@{ user.username }</p>
-		</div>
-		<div class="description">{ user.description }</div>
-		<div class="status">
-			<div>
-				<p>投稿</p><a>{ user.posts_count }</a>
-			</div>
-			<div>
-				<p>フォロー</p><a>{ user.following_count }</a>
-			</div>
-			<div>
-				<p>フォロワー</p><a>{ user.followers_count }</a>
-			</div>
-		</div>
-		<mk-follow-button v-if="$root.$data.os.isSignedIn && user.id != I.id" user={ userPromise }/>
-	</template>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			position absolute
-			z-index 2048
-			margin-top -8px
-			width 250px
-			background #fff
-			background-clip content-box
-			border solid 1px rgba(0, 0, 0, 0.1)
-			border-radius 4px
-			overflow hidden
-			opacity 0
-
-			> .banner
-				height 84px
-				background-color #f5f5f5
-				background-size cover
-				background-position center
-
-			> .avatar
-				display block
-				position absolute
-				top 62px
-				left 13px
-
-				> img
-					display block
-					width 58px
-					height 58px
-					margin 0
-					border solid 3px #fff
-					border-radius 8px
-
-			> .title
-				display block
-				padding 8px 0 8px 82px
-
-				> .name
-					display block
-					margin 0
-					font-weight bold
-					line-height 16px
-					color #656565
-
-				> .username
-					display block
-					margin 0
-					line-height 16px
-					font-size 0.8em
-					color #999
-
-			> .description
-				padding 0 16px
-				font-size 0.7em
-				color #555
-
-			> .status
-				padding 8px 16px
-
-				> div
-					display inline-block
-					width 33%
-
-					> p
-						margin 0
-						font-size 0.7em
-						color #aaa
-
-					> a
-						font-size 1em
-						color $theme-color
-
-			> mk-follow-button
-				position absolute
-				top 92px
-				right 8px
-
-	</style>
-	<script lang="typescript">
-		import * as anime from 'animejs';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.u = this.opts.user;
-		this.user = null;
-		this.userPromise =
-			typeof this.u == 'string' ?
-				new Promise((resolve, reject) => {
-					this.$root.$data.os.api('users/show', {
-						user_id: this.u[0] == '@' ? undefined : this.u,
-						username: this.u[0] == '@' ? this.u.substr(1) : undefined
-					}).then(resolve);
-				})
-			: Promise.resolve(this.u);
-
-		this.on('mount', () => {
-			this.userPromise.then(user => {
-				this.update({
-					user: user
-				});
-				this.open();
-			});
-		});
-
-		this.open = () => {
-			anime({
-				targets: this.root,
-				opacity: 1,
-				'margin-top': 0,
-				duration: 200,
-				easing: 'easeOutQuad'
-			});
-		};
-
-		this.close = () => {
-			anime({
-				targets: this.root,
-				opacity: 0,
-				'margin-top': '-8px',
-				duration: 200,
-				easing: 'easeOutQuad',
-				complete: () => this.$destroy()
-			});
-		};
-	</script>
-</mk-user-preview>
diff --git a/src/web/app/desktop/views/components/user-preview.vue b/src/web/app/desktop/views/components/user-preview.vue
new file mode 100644
index 000000000..fb6ae2553
--- /dev/null
+++ b/src/web/app/desktop/views/components/user-preview.vue
@@ -0,0 +1,160 @@
+<template>
+<div class="mk-user-preview">
+	<template v-if="u != null">
+		<div class="banner" :style="u.banner_url ? `background-image: url(${u.banner_url}?thumbnail&size=512)` : ''"></div>
+		<a class="avatar" :href="`/${u.username}`" target="_blank">
+			<img :src="`${u.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		</a>
+		<div class="title">
+			<p class="name">{{ u.name }}</p>
+			<p class="username">@{{ u.username }}</p>
+		</div>
+		<div class="description">{{ u.description }}</div>
+		<div class="status">
+			<div>
+				<p>投稿</p><a>{{ u.posts_count }}</a>
+			</div>
+			<div>
+				<p>フォロー</p><a>{{ u.following_count }}</a>
+			</div>
+			<div>
+				<p>フォロワー</p><a>{{ u.followers_count }}</a>
+			</div>
+		</div>
+		<mk-follow-button v-if="$root.$data.os.isSignedIn && user.id != $root.$data.os.i.id" :user="u"/>
+	</template>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+
+export default Vue.extend({
+	props: {
+		user: {
+			type: [Object, String],
+			required: true
+		}
+	},
+	data() {
+		return {
+			u: null
+		};
+	},
+	mounted() {
+		if (typeof this.user == 'object') {
+			this.u = this.user;
+			this.open();
+		} else {
+			this.$root.$data.os.api('users/show', {
+				user_id: this.user[0] == '@' ? undefined : this.user,
+				username: this.user[0] == '@' ? this.user.substr(1) : undefined
+			}).then(user => {
+				this.u = user;
+				this.open();
+			});
+		}
+	},
+	methods: {
+		open() {
+			anime({
+				targets: this.$el,
+				opacity: 1,
+				'margin-top': 0,
+				duration: 200,
+				easing: 'easeOutQuad'
+			});
+		},
+		close() {
+			anime({
+				targets: this.$el,
+				opacity: 0,
+				'margin-top': '-8px',
+				duration: 200,
+				easing: 'easeOutQuad',
+				complete: () => this.$destroy()
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-preview
+	position absolute
+	z-index 2048
+	margin-top -8px
+	width 250px
+	background #fff
+	background-clip content-box
+	border solid 1px rgba(0, 0, 0, 0.1)
+	border-radius 4px
+	overflow hidden
+	opacity 0
+
+	> .banner
+		height 84px
+		background-color #f5f5f5
+		background-size cover
+		background-position center
+
+	> .avatar
+		display block
+		position absolute
+		top 62px
+		left 13px
+
+		> img
+			display block
+			width 58px
+			height 58px
+			margin 0
+			border solid 3px #fff
+			border-radius 8px
+
+	> .title
+		display block
+		padding 8px 0 8px 82px
+
+		> .name
+			display block
+			margin 0
+			font-weight bold
+			line-height 16px
+			color #656565
+
+		> .username
+			display block
+			margin 0
+			line-height 16px
+			font-size 0.8em
+			color #999
+
+	> .description
+		padding 0 16px
+		font-size 0.7em
+		color #555
+
+	> .status
+		padding 8px 16px
+
+		> div
+			display inline-block
+			width 33%
+
+			> p
+				margin 0
+				font-size 0.7em
+				color #aaa
+
+			> a
+				font-size 1em
+				color $theme-color
+
+	> .mk-follow-button
+		position absolute
+		top 92px
+		right 8px
+
+</style>

From 697cbc3aa74030a52f897930404db08aed0ee513 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 17:01:36 +0900
Subject: [PATCH 156/286] wip

---
 .../app/desktop/-tags/messaging/window.tag    | 34 -------------------
 .../views/components/messaging-window.vue     | 33 ++++++++++++++++++
 .../views/components/post-form-window.vue     | 15 ++++----
 .../app/desktop/views/components/window.vue   |  4 ++-
 4 files changed, 43 insertions(+), 43 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/messaging/window.tag
 create mode 100644 src/web/app/desktop/views/components/messaging-window.vue

diff --git a/src/web/app/desktop/-tags/messaging/window.tag b/src/web/app/desktop/-tags/messaging/window.tag
deleted file mode 100644
index e078bccad..000000000
--- a/src/web/app/desktop/-tags/messaging/window.tag
+++ /dev/null
@@ -1,34 +0,0 @@
-<mk-messaging-window>
-	<mk-window ref="window" is-modal={ false } width={ '500px' } height={ '560px' }>
-		<yield to="header">%fa:comments%メッセージ</yield>
-		<yield to="content">
-			<mk-messaging ref="index"/>
-		</yield>
-	</mk-window>
-	<style lang="stylus" scoped>
-		:scope
-			> mk-window
-				[data-yield='header']
-					> [data-fa]
-						margin-right 4px
-
-				[data-yield='content']
-					> mk-messaging
-						height 100%
-						overflow auto
-
-	</style>
-	<script lang="typescript">
-		this.on('mount', () => {
-			this.$refs.window.on('closed', () => {
-				this.$destroy();
-			});
-
-			this.$refs.window.refs.index.on('navigate-user', user => {
-				riot.mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
-					user: user
-				});
-			});
-		});
-	</script>
-</mk-messaging-window>
diff --git a/src/web/app/desktop/views/components/messaging-window.vue b/src/web/app/desktop/views/components/messaging-window.vue
new file mode 100644
index 000000000..f8df20bc1
--- /dev/null
+++ b/src/web/app/desktop/views/components/messaging-window.vue
@@ -0,0 +1,33 @@
+<template>
+<mk-window ref="window" width='500px' height='560px' @closed="$destroy">
+	<span slot="header" :class="$style.header">%fa:comments%メッセージ</span>
+	<mk-messaging :class="$style.content" @navigate="navigate"/>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	methods: {
+		navigate(user) {
+			document.body.appendChild(new MkMessagingRoomWindow({
+				parent: this,
+				propsData: {
+					user: user
+				}
+			}).$mount().$el);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.header
+	> [data-fa]
+		margin-right 4px
+
+.content
+	height 100%
+	overflow auto
+
+</style>
diff --git a/src/web/app/desktop/views/components/post-form-window.vue b/src/web/app/desktop/views/components/post-form-window.vue
index 127233370..8647a8d2d 100644
--- a/src/web/app/desktop/views/components/post-form-window.vue
+++ b/src/web/app/desktop/views/components/post-form-window.vue
@@ -6,14 +6,13 @@
 		<span :class="$style.count" v-if="media.length != 0">{{ '%i18n:desktop.tags.mk-post-form-window.attaches%'.replace('{}', media.length) }}</span>
 		<span :class="$style.count" v-if="uploadings.length != 0">{{ '%i18n:desktop.tags.mk-post-form-window.uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
 	</span>
-	<div slot="content">
-		<mk-post-preview v-if="reply" :class="$style.postPreview" :post="reply"/>
-		<mk-post-form ref="form"
-			:reply="reply"
-			@posted="onPosted"
-			@change-uploadings="onChangeUploadings"
-			@change-attached-media="onChangeMedia"/>
-	</div>
+
+	<mk-post-preview v-if="reply" :class="$style.postPreview" :post="reply"/>
+	<mk-post-form ref="form"
+		:reply="reply"
+		@posted="onPosted"
+		@change-uploadings="onChangeUploadings"
+		@change-attached-media="onChangeMedia"/>
 </mk-window>
 </template>
 
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 069d4c4f9..946590d68 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -10,7 +10,9 @@
 					<button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="閉じる">%fa:times%</button>
 				</div>
 			</header>
-			<div class="content"><slot name="content"></slot></div>
+			<div class="content">
+				<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>

From 7c30b94cbb4f9374a1b8c7d3aa72e4612b1283a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 17:17:05 +0900
Subject: [PATCH 157/286] wip

---
 src/web/app/desktop/-tags/user-timeline.tag   | 150 ------------------
 .../views/components/user-timeline.vue        | 133 ++++++++++++++++
 2 files changed, 133 insertions(+), 150 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/user-timeline.tag
 create mode 100644 src/web/app/desktop/views/components/user-timeline.vue

diff --git a/src/web/app/desktop/-tags/user-timeline.tag b/src/web/app/desktop/-tags/user-timeline.tag
deleted file mode 100644
index 1071b6e2b..000000000
--- a/src/web/app/desktop/-tags/user-timeline.tag
+++ /dev/null
@@ -1,150 +0,0 @@
-<mk-user-timeline>
-	<header>
-		<span data-is-active={ mode == 'default' } @click="setMode.bind(this, 'default')">投稿</span><span data-is-active={ mode == 'with-replies' } @click="setMode.bind(this, 'with-replies')">投稿と返信</span>
-	</header>
-	<div class="loading" v-if="isLoading">
-		<mk-ellipsis-icon/>
-	</div>
-	<p class="empty" v-if="isEmpty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p>
-	<mk-timeline ref="timeline">
-		<yield to="footer">
-			<template v-if="!parent.moreLoading">%fa:moon%</template>
-			<template v-if="parent.moreLoading">%fa:spinner .pulse .fw%</template>
-		</yield/>
-	</mk-timeline>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-
-			> header
-				padding 8px 16px
-				border-bottom solid 1px #eee
-
-				> span
-					margin-right 16px
-					line-height 27px
-					font-size 18px
-					color #555
-
-					&:not([data-is-active])
-						color $theme-color
-						cursor pointer
-
-						&:hover
-							text-decoration underline
-
-			> .loading
-				padding 64px 0
-
-			> .empty
-				display block
-				margin 0 auto
-				padding 32px
-				max-width 400px
-				text-align center
-				color #999
-
-				> [data-fa]
-					display block
-					margin-bottom 16px
-					font-size 3em
-					color #ccc
-
-	</style>
-	<script lang="typescript">
-		import isPromise from '../../common/scripts/is-promise';
-
-		this.mixin('api');
-
-		this.user = null;
-		this.userPromise = isPromise(this.opts.user)
-			? this.opts.user
-			: Promise.resolve(this.opts.user);
-		this.isLoading = true;
-		this.isEmpty = false;
-		this.moreLoading = false;
-		this.unreadCount = 0;
-		this.mode = 'default';
-
-		this.on('mount', () => {
-			document.addEventListener('keydown', this.onDocumentKeydown);
-			window.addEventListener('scroll', this.onScroll);
-
-			this.userPromise.then(user => {
-				this.update({
-					user: user
-				});
-
-				this.fetch(() => this.$emit('loaded'));
-			});
-		});
-
-		this.on('unmount', () => {
-			document.removeEventListener('keydown', this.onDocumentKeydown);
-			window.removeEventListener('scroll', this.onScroll);
-		});
-
-		this.onDocumentKeydown = e => {
-			if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') {
-				if (e.which == 84) { // [t]
-					this.$refs.timeline.focus();
-				}
-			}
-		};
-
-		this.fetch = cb => {
-			this.$root.$data.os.api('users/posts', {
-				user_id: this.user.id,
-				until_date: this.date ? this.date.getTime() : undefined,
-				with_replies: this.mode == 'with-replies'
-			}).then(posts => {
-				this.update({
-					isLoading: false,
-					isEmpty: posts.length == 0
-				});
-				this.$refs.timeline.setPosts(posts);
-				if (cb) cb();
-			});
-		};
-
-		this.more = () => {
-			if (this.moreLoading || this.isLoading || this.$refs.timeline.posts.length == 0) return;
-			this.update({
-				moreLoading: true
-			});
-			this.$root.$data.os.api('users/posts', {
-				user_id: this.user.id,
-				with_replies: this.mode == 'with-replies',
-				until_id: this.$refs.timeline.tail().id
-			}).then(posts => {
-				this.update({
-					moreLoading: false
-				});
-				this.$refs.timeline.prependPosts(posts);
-			});
-		};
-
-		this.onScroll = () => {
-			const current = window.scrollY + window.innerHeight;
-			if (current > document.body.offsetHeight - 16/*遊び*/) {
-				this.more();
-			}
-		};
-
-		this.setMode = mode => {
-			this.update({
-				mode: mode
-			});
-			this.fetch();
-		};
-
-		this.warp = date => {
-			this.update({
-				date: date
-			});
-
-			this.fetch();
-		};
-	</script>
-</mk-user-timeline>
diff --git a/src/web/app/desktop/views/components/user-timeline.vue b/src/web/app/desktop/views/components/user-timeline.vue
new file mode 100644
index 000000000..bab32fd24
--- /dev/null
+++ b/src/web/app/desktop/views/components/user-timeline.vue
@@ -0,0 +1,133 @@
+<template>
+<div class="mk-user-timeline">
+	<header>
+		<span :data-is-active="mode == 'default'" @click="mode = 'default'">投稿</span>
+		<span :data-is-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿と返信</span>
+	</header>
+	<div class="loading" v-if="fetching">
+		<mk-ellipsis-icon/>
+	</div>
+	<p class="empty" v-if="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p>
+	<mk-posts ref="timeline" :posts="posts">
+		<div slot="footer">
+			<template v-if="!moreFetching">%fa:moon%</template>
+			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
+		</div>
+	</mk-posts>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user'],
+	data() {
+		return {
+			fetching: true,
+			moreFetching: false,
+			mode: 'default',
+			unreadCount: 0,
+			posts: [],
+			date: null
+		};
+	},
+	watch: {
+		mode() {
+			this.fetch();
+		}
+	},
+	computed: {
+		empty(): boolean {
+			return this.posts.length == 0;
+		}
+	},
+	mounted() {
+		document.addEventListener('keydown', this.onDocumentKeydown);
+		window.addEventListener('scroll', this.onScroll);
+
+		this.fetch(() => this.$emit('loaded'));
+	},
+	beforeDestroy() {
+		document.removeEventListener('keydown', this.onDocumentKeydown);
+		window.removeEventListener('scroll', this.onScroll);
+	},
+	methods: {
+		onDocumentKeydown(e) {
+			if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') {
+				if (e.which == 84) { // [t]
+					(this.$refs.timeline as any).focus();
+				}
+			}
+		},
+		fetch(cb?) {
+			this.$root.$data.os.api('users/posts', {
+				user_id: this.user.id,
+				until_date: this.date ? this.date.getTime() : undefined,
+				with_replies: this.mode == 'with-replies'
+			}).then(posts => {
+				this.fetching = false;
+				this.posts = posts;
+				if (cb) cb();
+			});
+		},
+		more() {
+			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
+			this.moreFetching = true;
+			this.$root.$data.os.api('users/posts', {
+				user_id: this.user.id,
+				with_replies: this.mode == 'with-replies',
+				until_id: this.posts[this.posts.length - 1].id
+			}).then(posts => {
+				this.moreFetching = false;
+				this.posts = this.posts.concat(posts);
+			});
+		},
+		onScroll() {
+			const current = window.scrollY + window.innerHeight;
+			if (current > document.body.offsetHeight - 16/*遊び*/) {
+				this.more();
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-timeline
+	background #fff
+
+	> header
+		padding 8px 16px
+		border-bottom solid 1px #eee
+
+		> span
+			margin-right 16px
+			line-height 27px
+			font-size 18px
+			color #555
+
+			&:not([data-is-active])
+				color $theme-color
+				cursor pointer
+
+				&:hover
+					text-decoration underline
+
+	> .loading
+		padding 64px 0
+
+	> .empty
+		display block
+		margin 0 auto
+		padding 32px
+		max-width 400px
+		text-align center
+		color #999
+
+		> [data-fa]
+			display block
+			margin-bottom 16px
+			font-size 3em
+			color #ccc
+
+</style>

From 4be168ad51a8088ffab85d92f91ace0200e4c5ef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 17:20:55 +0900
Subject: [PATCH 158/286] wip

---
 .../desktop/-tags/pages/messaging-room.tag    | 37 ----------------
 .../desktop/views/pages/messaging-room.vue    | 42 +++++++++++++++++++
 2 files changed, 42 insertions(+), 37 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/pages/messaging-room.tag
 create mode 100644 src/web/app/desktop/views/pages/messaging-room.vue

diff --git a/src/web/app/desktop/-tags/pages/messaging-room.tag b/src/web/app/desktop/-tags/pages/messaging-room.tag
deleted file mode 100644
index cfacc4a1b..000000000
--- a/src/web/app/desktop/-tags/pages/messaging-room.tag
+++ /dev/null
@@ -1,37 +0,0 @@
-<mk-messaging-room-page>
-	<mk-messaging-room v-if="user" user={ user } is-naked={ true }/>
-
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-
-	</style>
-	<script lang="typescript">
-		import Progress from '../../../common/scripts/loading';
-
-		this.mixin('api');
-
-		this.fetching = true;
-		this.user = null;
-
-		this.on('mount', () => {
-			Progress.start();
-
-			document.documentElement.style.background = '#fff';
-
-			this.$root.$data.os.api('users/show', {
-				username: this.opts.user
-			}).then(user => {
-				this.update({
-					fetching: false,
-					user: user
-				});
-
-				document.title = 'メッセージ: ' + this.user.name;
-
-				Progress.done();
-			});
-		});
-	</script>
-</mk-messaging-room-page>
diff --git a/src/web/app/desktop/views/pages/messaging-room.vue b/src/web/app/desktop/views/pages/messaging-room.vue
new file mode 100644
index 000000000..86230cb54
--- /dev/null
+++ b/src/web/app/desktop/views/pages/messaging-room.vue
@@ -0,0 +1,42 @@
+<template>
+<div class="mk-messaging-room-page">
+	<mk-messaging-room v-if="user" :user="user" is-naked/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+	props: ['username'],
+	data() {
+		return {
+			fetching: true,
+			user: null
+		};
+	},
+	mounted() {
+		Progress.start();
+
+		document.documentElement.style.background = '#fff';
+
+		this.$root.$data.os.api('users/show', {
+			username: this.username
+		}).then(user => {
+			this.fetching = false;
+			this.user = user;
+
+			document.title = 'メッセージ: ' + this.user.name;
+
+			Progress.done();
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-messaging-room-page
+	background #fff
+
+</style>

From fad3132d1862dff4f1e2d808efaa23ddbad8cb12 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 17:22:39 +0900
Subject: [PATCH 159/286] wip

---
 src/web/app/desktop/-tags/pages/home-customize.tag | 12 ------------
 src/web/app/desktop/views/pages/home-custmize.vue  | 12 ++++++++++++
 2 files changed, 12 insertions(+), 12 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/pages/home-customize.tag
 create mode 100644 src/web/app/desktop/views/pages/home-custmize.vue

diff --git a/src/web/app/desktop/-tags/pages/home-customize.tag b/src/web/app/desktop/-tags/pages/home-customize.tag
deleted file mode 100644
index 178558f9d..000000000
--- a/src/web/app/desktop/-tags/pages/home-customize.tag
+++ /dev/null
@@ -1,12 +0,0 @@
-<mk-home-customize-page>
-	<mk-home ref="home" mode="timeline" customize={ true }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		this.on('mount', () => {
-			document.title = 'Misskey - ホームのカスタマイズ';
-		});
-	</script>
-</mk-home-customize-page>
diff --git a/src/web/app/desktop/views/pages/home-custmize.vue b/src/web/app/desktop/views/pages/home-custmize.vue
new file mode 100644
index 000000000..257e83cad
--- /dev/null
+++ b/src/web/app/desktop/views/pages/home-custmize.vue
@@ -0,0 +1,12 @@
+<template>
+	<mk-home customize/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	mounted() {
+		document.title = 'Misskey - ホームのカスタマイズ';
+	}
+});
+</script>

From c377ddf77a5cae98b04444dc93e738e06b9bd6e6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 17:35:15 +0900
Subject: [PATCH 160/286] wip

---
 src/web/app/desktop/-tags/pages/search.tag |  20 ----
 src/web/app/desktop/-tags/search-posts.tag |  96 -----------------
 src/web/app/desktop/-tags/search.tag       |  34 ------
 src/web/app/desktop/views/pages/search.vue | 115 +++++++++++++++++++++
 4 files changed, 115 insertions(+), 150 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/pages/search.tag
 delete mode 100644 src/web/app/desktop/-tags/search-posts.tag
 delete mode 100644 src/web/app/desktop/-tags/search.tag
 create mode 100644 src/web/app/desktop/views/pages/search.vue

diff --git a/src/web/app/desktop/-tags/pages/search.tag b/src/web/app/desktop/-tags/pages/search.tag
deleted file mode 100644
index eaa80a039..000000000
--- a/src/web/app/desktop/-tags/pages/search.tag
+++ /dev/null
@@ -1,20 +0,0 @@
-<mk-search-page>
-	<mk-ui ref="ui">
-		<mk-search ref="search" query={ parent.opts.query }/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import Progress from '../../../common/scripts/loading';
-
-		this.on('mount', () => {
-			Progress.start();
-
-			this.$refs.ui.refs.search.on('loaded', () => {
-				Progress.done();
-			});
-		});
-	</script>
-</mk-search-page>
diff --git a/src/web/app/desktop/-tags/search-posts.tag b/src/web/app/desktop/-tags/search-posts.tag
deleted file mode 100644
index 94a6f2524..000000000
--- a/src/web/app/desktop/-tags/search-posts.tag
+++ /dev/null
@@ -1,96 +0,0 @@
-<mk-search-posts>
-	<div class="loading" v-if="isLoading">
-		<mk-ellipsis-icon/>
-	</div>
-	<p class="empty" v-if="isEmpty">%fa:search%「{ query }」に関する投稿は見つかりませんでした。</p>
-	<mk-timeline ref="timeline">
-		<yield to="footer">
-			<template v-if="!parent.moreLoading">%fa:moon%</template>
-			<template v-if="parent.moreLoading">%fa:spinner .pulse .fw%</template>
-		</yield/>
-	</mk-timeline>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-
-			> .loading
-				padding 64px 0
-
-			> .empty
-				display block
-				margin 0 auto
-				padding 32px
-				max-width 400px
-				text-align center
-				color #999
-
-				> [data-fa]
-					display block
-					margin-bottom 16px
-					font-size 3em
-					color #ccc
-
-	</style>
-	<script lang="typescript">
-		import parse from '../../common/scripts/parse-search-query';
-
-		this.mixin('api');
-
-		this.query = this.opts.query;
-		this.isLoading = true;
-		this.isEmpty = false;
-		this.moreLoading = false;
-		this.limit = 30;
-		this.offset = 0;
-
-		this.on('mount', () => {
-			document.addEventListener('keydown', this.onDocumentKeydown);
-			window.addEventListener('scroll', this.onScroll);
-
-			this.$root.$data.os.api('posts/search', parse(this.query)).then(posts => {
-				this.update({
-					isLoading: false,
-					isEmpty: posts.length == 0
-				});
-				this.$refs.timeline.setPosts(posts);
-				this.$emit('loaded');
-			});
-		});
-
-		this.on('unmount', () => {
-			document.removeEventListener('keydown', this.onDocumentKeydown);
-			window.removeEventListener('scroll', this.onScroll);
-		});
-
-		this.onDocumentKeydown = e => {
-			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
-				if (e.which == 84) { // t
-					this.$refs.timeline.focus();
-				}
-			}
-		};
-
-		this.more = () => {
-			if (this.moreLoading || this.isLoading || this.timeline.posts.length == 0) return;
-			this.offset += this.limit;
-			this.update({
-				moreLoading: true
-			});
-			return this.$root.$data.os.api('posts/search', Object.assign({}, parse(this.query), {
-				limit: this.limit,
-				offset: this.offset
-			})).then(posts => {
-				this.update({
-					moreLoading: false
-				});
-				this.$refs.timeline.prependPosts(posts);
-			});
-		};
-
-		this.onScroll = () => {
-			const current = window.scrollY + window.innerHeight;
-			if (current > document.body.offsetHeight - 16) this.more();
-		};
-	</script>
-</mk-search-posts>
diff --git a/src/web/app/desktop/-tags/search.tag b/src/web/app/desktop/-tags/search.tag
deleted file mode 100644
index 28127b721..000000000
--- a/src/web/app/desktop/-tags/search.tag
+++ /dev/null
@@ -1,34 +0,0 @@
-<mk-search>
-	<header>
-		<h1>{ query }</h1>
-	</header>
-	<mk-search-posts ref="posts" query={ query }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding-bottom 32px
-
-			> header
-				width 100%
-				max-width 600px
-				margin 0 auto
-				color #555
-
-			> mk-search-posts
-				max-width 600px
-				margin 0 auto
-				border solid 1px rgba(0, 0, 0, 0.075)
-				border-radius 6px
-				overflow hidden
-
-	</style>
-	<script lang="typescript">
-		this.query = this.opts.query;
-
-		this.on('mount', () => {
-			this.$refs.posts.on('loaded', () => {
-				this.$emit('loaded');
-			});
-		});
-	</script>
-</mk-search>
diff --git a/src/web/app/desktop/views/pages/search.vue b/src/web/app/desktop/views/pages/search.vue
new file mode 100644
index 000000000..d8147e0d6
--- /dev/null
+++ b/src/web/app/desktop/views/pages/search.vue
@@ -0,0 +1,115 @@
+<template>
+<mk-ui>
+	<header :class="$style.header">
+		<h1>{{ query }}</h1>
+	</header>
+	<div :class="$style.loading" v-if="fetching">
+		<mk-ellipsis-icon/>
+	</div>
+	<p :class="$style.empty" v-if="empty">%fa:search%「{{ query }}」に関する投稿は見つかりませんでした。</p>
+	<mk-posts ref="timeline" :class="$style.posts">
+		<div slot="footer">
+			<template v-if="!moreFetching">%fa:search%</template>
+			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
+		</div>
+	</mk-posts>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+import parse from '../../../common/scripts/parse-search-query';
+
+const limit = 30;
+
+export default Vue.extend({
+	props: ['query'],
+	data() {
+		return {
+			fetching: true,
+			moreFetching: false,
+			offset: 0,
+			posts: []
+		};
+	},
+	computed: {
+		empty(): boolean {
+			return this.posts.length == 0;
+		}
+	},
+	mounted() {
+		Progress.start();
+
+		document.addEventListener('keydown', this.onDocumentKeydown);
+		window.addEventListener('scroll', this.onScroll);
+
+		this.$root.$data.os.api('posts/search', parse(this.query)).then(posts => {
+			this.fetching = false;
+			this.posts = posts;
+		});
+	},
+	beforeDestroy() {
+		document.removeEventListener('keydown', this.onDocumentKeydown);
+		window.removeEventListener('scroll', this.onScroll);
+	},
+	methods: {
+		onDocumentKeydown(e) {
+			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
+				if (e.which == 84) { // t
+					(this.$refs.timeline as any).focus();
+				}
+			}
+		},
+		more() {
+			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
+			this.offset += limit;
+			this.moreFetching = true;
+			return this.$root.$data.os.api('posts/search', Object.assign({}, parse(this.query), {
+				limit: limit,
+				offset: this.offset
+			})).then(posts => {
+				this.moreFetching = false;
+				this.posts = this.posts.concat(posts);
+			});
+		},
+		onScroll() {
+			const current = window.scrollY + window.innerHeight;
+			if (current > document.body.offsetHeight - 16) this.more();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.header
+	width 100%
+	max-width 600px
+	margin 0 auto
+	color #555
+
+.posts
+	max-width 600px
+	margin 0 auto
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+	overflow hidden
+
+.loading
+	padding 64px 0
+
+.empty
+	display block
+	margin 0 auto
+	padding 32px
+	max-width 400px
+	text-align center
+	color #999
+
+	> [data-fa]
+		display block
+		margin-bottom 16px
+		font-size 3em
+		color #ccc
+
+</style>

From 09a388dead2b8d8e560d0fb707fa9f51ec23a5f4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 17:43:25 +0900
Subject: [PATCH 161/286] wip

---
 .../desktop/-tags/user-followers-window.tag   | 19 --------------
 .../desktop/-tags/user-following-window.tag   | 19 --------------
 .../views/components/followers-window.vue     | 26 +++++++++++++++++++
 .../views/components/following-window.vue     | 26 +++++++++++++++++++
 4 files changed, 52 insertions(+), 38 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/user-followers-window.tag
 delete mode 100644 src/web/app/desktop/-tags/user-following-window.tag
 create mode 100644 src/web/app/desktop/views/components/followers-window.vue
 create mode 100644 src/web/app/desktop/views/components/following-window.vue

diff --git a/src/web/app/desktop/-tags/user-followers-window.tag b/src/web/app/desktop/-tags/user-followers-window.tag
deleted file mode 100644
index 82bec6992..000000000
--- a/src/web/app/desktop/-tags/user-followers-window.tag
+++ /dev/null
@@ -1,19 +0,0 @@
-<mk-user-followers-window>
-	<mk-window is-modal={ false } width={ '400px' } height={ '550px' }><yield to="header"><img src={ parent.user.avatar_url + '?thumbnail&size=64' } alt=""/>{ parent.user.name }のフォロワー</yield>
-<yield to="content">
-		<mk-user-followers user={ parent.user }/></yield>
-	</mk-window>
-	<style lang="stylus" scoped>
-		:scope
-			> mk-window
-				[data-yield='header']
-					> img
-						display inline-block
-						vertical-align bottom
-						height calc(100% - 10px)
-						margin 5px
-						border-radius 4px
-
-	</style>
-	<script lang="typescript">this.user = this.opts.user</script>
-</mk-user-followers-window>
diff --git a/src/web/app/desktop/-tags/user-following-window.tag b/src/web/app/desktop/-tags/user-following-window.tag
deleted file mode 100644
index 0f1c4b3ea..000000000
--- a/src/web/app/desktop/-tags/user-following-window.tag
+++ /dev/null
@@ -1,19 +0,0 @@
-<mk-user-following-window>
-	<mk-window is-modal={ false } width={ '400px' } height={ '550px' }><yield to="header"><img src={ parent.user.avatar_url + '?thumbnail&size=64' } alt=""/>{ parent.user.name }のフォロー</yield>
-<yield to="content">
-		<mk-user-following user={ parent.user }/></yield>
-	</mk-window>
-	<style lang="stylus" scoped>
-		:scope
-			> mk-window
-				[data-yield='header']
-					> img
-						display inline-block
-						vertical-align bottom
-						height calc(100% - 10px)
-						margin 5px
-						border-radius 4px
-
-	</style>
-	<script lang="typescript">this.user = this.opts.user</script>
-</mk-user-following-window>
diff --git a/src/web/app/desktop/views/components/followers-window.vue b/src/web/app/desktop/views/components/followers-window.vue
new file mode 100644
index 000000000..e56545ccc
--- /dev/null
+++ b/src/web/app/desktop/views/components/followers-window.vue
@@ -0,0 +1,26 @@
+<template>
+<mk-window width='400px' height='550px' @closed="$destroy">
+	<span slot="header" :class="$style.header">
+		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロワー
+	</span>
+	<mk-user-followers :user="user"/>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user']
+});
+</script>
+
+<style lang="stylus" module>
+.header
+	> img
+		display inline-block
+		vertical-align bottom
+		height calc(100% - 10px)
+		margin 5px
+		border-radius 4px
+
+</style>
diff --git a/src/web/app/desktop/views/components/following-window.vue b/src/web/app/desktop/views/components/following-window.vue
new file mode 100644
index 000000000..fa2edfa47
--- /dev/null
+++ b/src/web/app/desktop/views/components/following-window.vue
@@ -0,0 +1,26 @@
+<template>
+<mk-window width='400px' height='550px' @closed="$destroy">
+	<span slot="header" :class="$style.header">
+		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロー
+	</span>
+	<mk-user-following :user="user"/>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user']
+});
+</script>
+
+<style lang="stylus" module>
+.header
+	> img
+		display inline-block
+		vertical-align bottom
+		height calc(100% - 10px)
+		margin 5px
+		border-radius 4px
+
+</style>

From 87e073a3597b387c8638e73593e295b4c6c8ed87 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 18:00:32 +0900
Subject: [PATCH 162/286] wip

---
 .../-tags/select-file-from-drive-window.tag   | 173 -----------------
 .../choose-file-from-drive-window.vue         | 175 ++++++++++++++++++
 2 files changed, 175 insertions(+), 173 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/select-file-from-drive-window.tag
 create mode 100644 src/web/app/desktop/views/components/choose-file-from-drive-window.vue

diff --git a/src/web/app/desktop/-tags/select-file-from-drive-window.tag b/src/web/app/desktop/-tags/select-file-from-drive-window.tag
deleted file mode 100644
index d6234d5fd..000000000
--- a/src/web/app/desktop/-tags/select-file-from-drive-window.tag
+++ /dev/null
@@ -1,173 +0,0 @@
-<mk-select-file-from-drive-window>
-	<mk-window ref="window" is-modal={ true } width={ '800px' } height={ '500px' }>
-		<yield to="header">
-			<mk-raw content={ parent.title }/>
-			<span class="count" v-if="parent.multiple && parent.files.length > 0">({ parent.files.length }ファイル選択中)</span>
-		</yield>
-		<yield to="content">
-			<mk-drive-browser ref="browser" multiple={ parent.multiple }/>
-			<div>
-				<button class="upload" title="PCからドライブにファイルをアップロード" @click="parent.upload">%fa:upload%</button>
-				<button class="cancel" @click="parent.close">キャンセル</button>
-				<button class="ok" disabled={ parent.multiple && parent.files.length == 0 } @click="parent.ok">決定</button>
-			</div>
-		</yield>
-	</mk-window>
-	<style lang="stylus" scoped>
-		:scope
-			> mk-window
-				[data-yield='header']
-					> mk-raw
-						> [data-fa]
-							margin-right 4px
-
-					.count
-						margin-left 8px
-						opacity 0.7
-
-				[data-yield='content']
-					> mk-drive-browser
-						height calc(100% - 72px)
-
-					> div
-						height 72px
-						background lighten($theme-color, 95%)
-
-						> .upload
-							display inline-block
-							position absolute
-							top 8px
-							left 16px
-							cursor pointer
-							padding 0
-							margin 8px 4px 0 0
-							width 40px
-							height 40px
-							font-size 1em
-							color rgba($theme-color, 0.5)
-							background transparent
-							outline none
-							border solid 1px transparent
-							border-radius 4px
-
-							&:hover
-								background transparent
-								border-color rgba($theme-color, 0.3)
-
-							&:active
-								color rgba($theme-color, 0.6)
-								background transparent
-								border-color rgba($theme-color, 0.5)
-								box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset
-
-							&:focus
-								&:after
-									content ""
-									pointer-events none
-									position absolute
-									top -5px
-									right -5px
-									bottom -5px
-									left -5px
-									border 2px solid rgba($theme-color, 0.3)
-									border-radius 8px
-
-						> .ok
-						> .cancel
-							display block
-							position absolute
-							bottom 16px
-							cursor pointer
-							padding 0
-							margin 0
-							width 120px
-							height 40px
-							font-size 1em
-							outline none
-							border-radius 4px
-
-							&:focus
-								&:after
-									content ""
-									pointer-events none
-									position absolute
-									top -5px
-									right -5px
-									bottom -5px
-									left -5px
-									border 2px solid rgba($theme-color, 0.3)
-									border-radius 8px
-
-							&:disabled
-								opacity 0.7
-								cursor default
-
-						> .ok
-							right 16px
-							color $theme-color-foreground
-							background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
-							border solid 1px lighten($theme-color, 15%)
-
-							&:not(:disabled)
-								font-weight bold
-
-							&:hover:not(:disabled)
-								background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
-								border-color $theme-color
-
-							&:active:not(:disabled)
-								background $theme-color
-								border-color $theme-color
-
-						> .cancel
-							right 148px
-							color #888
-							background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
-							border solid 1px #e2e2e2
-
-							&:hover
-								background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
-								border-color #dcdcdc
-
-							&:active
-								background #ececec
-								border-color #dcdcdc
-
-	</style>
-	<script lang="typescript">
-		this.files = [];
-
-		this.multiple = this.opts.multiple != null ? this.opts.multiple : false;
-		this.title = this.opts.title || '%fa:R file%ファイルを選択';
-
-		this.on('mount', () => {
-			this.$refs.window.refs.browser.on('selected', file => {
-				this.files = [file];
-				this.ok();
-			});
-
-			this.$refs.window.refs.browser.on('change-selection', files => {
-				this.update({
-					files: files
-				});
-			});
-
-			this.$refs.window.on('closed', () => {
-				this.$destroy();
-			});
-		});
-
-		this.close = () => {
-			this.$refs.window.close();
-		};
-
-		this.upload = () => {
-			this.$refs.window.refs.browser.selectLocalFile();
-		};
-
-		this.ok = () => {
-			this.$emit('selected', this.multiple ? this.files : this.files[0]);
-			this.$refs.window.close();
-		};
-	</script>
-</mk-select-file-from-drive-window>
diff --git a/src/web/app/desktop/views/components/choose-file-from-drive-window.vue b/src/web/app/desktop/views/components/choose-file-from-drive-window.vue
new file mode 100644
index 000000000..ed9ca6466
--- /dev/null
+++ b/src/web/app/desktop/views/components/choose-file-from-drive-window.vue
@@ -0,0 +1,175 @@
+<template>
+<mk-window ref="window" is-modal width='800px' height='500px' @closed="$destroy">
+	<span slot="header">
+		<span v-html="title" :class="$style.title"></span>
+		<span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}ファイル選択中)</span>
+	</span>
+
+	<mk-drive
+		ref="browser"
+		:class="$style.browser"
+		:multiple="multiple"
+		@selected="onSelected"
+		@change-selection="onChangeSelection"
+	/>
+	<div :class="$style.footer">
+		<button :class="$style.upload" title="PCからドライブにファイルをアップロード" @click="upload">%fa:upload%</button>
+		<button :class="$style.cancel" @click="close">キャンセル</button>
+		<button :class="$style.ok" :disabled="multiple && files.length == 0" @click="ok">決定</button>
+	</div>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		multiple: {
+			default: false
+		},
+		title: {
+			default: '%fa:R file%ファイルを選択'
+		}
+	},
+	data() {
+		return {
+			files: []
+		};
+	},
+	methods: {
+		onSelected(file) {
+			this.files = [file];
+			this.ok();
+		},
+		onChangeselection(files) {
+			this.files = files;
+		},
+		upload() {
+			(this.$refs.browser as any).selectLocalFile();
+		},
+		ok() {
+			this.$emit('selected', this.multiple ? this.files : this.files[0]);
+			(this.$refs.window as any).close();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.title
+	> [data-fa]
+		margin-right 4px
+
+.count
+	margin-left 8px
+	opacity 0.7
+
+.browser
+	height calc(100% - 72px)
+
+.footer
+	height 72px
+	background lighten($theme-color, 95%)
+
+.upload
+	display inline-block
+	position absolute
+	top 8px
+	left 16px
+	cursor pointer
+	padding 0
+	margin 8px 4px 0 0
+	width 40px
+	height 40px
+	font-size 1em
+	color rgba($theme-color, 0.5)
+	background transparent
+	outline none
+	border solid 1px transparent
+	border-radius 4px
+
+	&:hover
+		background transparent
+		border-color rgba($theme-color, 0.3)
+
+	&:active
+		color rgba($theme-color, 0.6)
+		background transparent
+		border-color rgba($theme-color, 0.5)
+		box-shadow 0 2px 4px rgba(darken($theme-color, 50%), 0.15) inset
+
+	&:focus
+		&:after
+			content ""
+			pointer-events none
+			position absolute
+			top -5px
+			right -5px
+			bottom -5px
+			left -5px
+			border 2px solid rgba($theme-color, 0.3)
+			border-radius 8px
+
+.ok
+.cancel
+	display block
+	position absolute
+	bottom 16px
+	cursor pointer
+	padding 0
+	margin 0
+	width 120px
+	height 40px
+	font-size 1em
+	outline none
+	border-radius 4px
+
+	&:focus
+		&:after
+			content ""
+			pointer-events none
+			position absolute
+			top -5px
+			right -5px
+			bottom -5px
+			left -5px
+			border 2px solid rgba($theme-color, 0.3)
+			border-radius 8px
+
+	&:disabled
+		opacity 0.7
+		cursor default
+
+.ok
+	right 16px
+	color $theme-color-foreground
+	background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+	border solid 1px lighten($theme-color, 15%)
+
+	&:not(:disabled)
+		font-weight bold
+
+	&:hover:not(:disabled)
+		background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
+		border-color $theme-color
+
+	&:active:not(:disabled)
+		background $theme-color
+		border-color $theme-color
+
+.cancel
+	right 148px
+	color #888
+	background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
+	border solid 1px #e2e2e2
+
+	&:hover
+		background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
+		border-color #dcdcdc
+
+	&:active
+		background #ececec
+		border-color #dcdcdc
+
+</style>
+

From d65e5541b30ccf77ed08fdc729336a00f2ea8358 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 20:32:22 +0900
Subject: [PATCH 163/286] wip

---
 .../desktop/-tags/messaging/room-window.tag   | 32 -------------------
 .../components/messaging-room-window.vue      | 31 ++++++++++++++++++
 2 files changed, 31 insertions(+), 32 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/messaging/room-window.tag
 create mode 100644 src/web/app/desktop/views/components/messaging-room-window.vue

diff --git a/src/web/app/desktop/-tags/messaging/room-window.tag b/src/web/app/desktop/-tags/messaging/room-window.tag
deleted file mode 100644
index ca1187364..000000000
--- a/src/web/app/desktop/-tags/messaging/room-window.tag
+++ /dev/null
@@ -1,32 +0,0 @@
-<mk-messaging-room-window>
-	<mk-window ref="window" is-modal={ false } width={ '500px' } height={ '560px' } popout={ popout }>
-		<yield to="header">%fa:comments%メッセージ: { parent.user.name }</yield>
-		<yield to="content">
-			<mk-messaging-room user={ parent.user }/>
-		</yield>
-	</mk-window>
-	<style lang="stylus" scoped>
-		:scope
-			> mk-window
-				[data-yield='header']
-					> [data-fa]
-						margin-right 4px
-
-				[data-yield='content']
-					> mk-messaging-room
-						height 100%
-						overflow auto
-
-	</style>
-	<script lang="typescript">
-		this.user = this.opts.user;
-
-		this.popout = `${_URL_}/i/messaging/${this.user.username}`;
-
-		this.on('mount', () => {
-			this.$refs.window.on('closed', () => {
-				this.$destroy();
-			});
-		});
-	</script>
-</mk-messaging-room-window>
diff --git a/src/web/app/desktop/views/components/messaging-room-window.vue b/src/web/app/desktop/views/components/messaging-room-window.vue
new file mode 100644
index 000000000..f93990d89
--- /dev/null
+++ b/src/web/app/desktop/views/components/messaging-room-window.vue
@@ -0,0 +1,31 @@
+<template>
+<mk-window ref="window" width="500px" height="560px" :popout="popout" @closed="$destroy">
+	<span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ user.name }}</span>
+	<mk-messaging-room :user="user" :class="$style.content"/>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { url } from '../../../config';
+
+export default Vue.extend({
+	props: ['user'],
+	computed: {
+		popout(): string {
+			return `${url}/i/messaging/${this.user.username}`;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.header
+	> [data-fa]
+		margin-right 4px
+
+.content
+	height 100%
+	overflow auto
+
+</style>

From e8f48bcec48219047845eb064752bd46d76b5578 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 20:53:15 +0900
Subject: [PATCH 164/286] wip

---
 src/web/app/mobile/tags/user-timeline.tag     | 33 -------------
 .../mobile/views/components/user-timeline.vue | 46 +++++++++++++++++++
 2 files changed, 46 insertions(+), 33 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/user-timeline.tag
 create mode 100644 src/web/app/mobile/views/components/user-timeline.vue

diff --git a/src/web/app/mobile/tags/user-timeline.tag b/src/web/app/mobile/tags/user-timeline.tag
deleted file mode 100644
index 546558155..000000000
--- a/src/web/app/mobile/tags/user-timeline.tag
+++ /dev/null
@@ -1,33 +0,0 @@
-<mk-user-timeline>
-	<mk-timeline ref="timeline" init={ init } more={ more } empty={ withMedia ? '%i18n:mobile.tags.mk-user-timeline.no-posts-with-media%' : '%i18n:mobile.tags.mk-user-timeline.no-posts%' }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			max-width 600px
-			margin 0 auto
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.user = this.opts.user;
-		this.withMedia = this.opts.withMedia;
-
-		this.init = new Promise((res, rej) => {
-			this.$root.$data.os.api('users/posts', {
-				user_id: this.user.id,
-				with_media: this.withMedia
-			}).then(posts => {
-				res(posts);
-				this.$emit('loaded');
-			});
-		});
-
-		this.more = () => {
-			return this.$root.$data.os.api('users/posts', {
-				user_id: this.user.id,
-				with_media: this.withMedia,
-				until_id: this.$refs.timeline.tail().id
-			});
-		};
-	</script>
-</mk-user-timeline>
diff --git a/src/web/app/mobile/views/components/user-timeline.vue b/src/web/app/mobile/views/components/user-timeline.vue
new file mode 100644
index 000000000..9a31ace4d
--- /dev/null
+++ b/src/web/app/mobile/views/components/user-timeline.vue
@@ -0,0 +1,46 @@
+<template>
+<div class="mk-user-timeline">
+	<mk-posts :posts="posts">
+		<div class="init" v-if="fetching">
+			%fa:spinner .pulse%%i18n:common.loading%
+		</div>
+		<div class="empty" v-if="!fetching && posts.length == 0">
+			%fa:R comments%
+			{{ withMedia ? '%i18n:mobile.tags.mk-user-timeline.no-posts-with-media%' : '%i18n:mobile.tags.mk-user-timeline.no-posts%' }}
+		</div>
+		<button v-if="canFetchMore" @click="more" :disabled="fetching" slot="tail">
+			<span v-if="!fetching">%i18n:mobile.tags.mk-user-timeline.load-more%</span>
+			<span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span>
+		</button>
+	</mk-posts>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['user', 'withMedia'],
+	data() {
+		return {
+			fetching: true,
+			posts: []
+		};
+	},
+	mounted() {
+		this.$root.$data.os.api('users/posts', {
+			user_id: this.user.id,
+			with_media: this.withMedia
+		}).then(posts => {
+			this.fetching = false;
+			this.posts = posts;
+			this.$emit('loaded');
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-user-timeline
+	max-width 600px
+	margin 0 auto
+</style>

From fa142e3e72ff343600cd9560e0898db1bc9dfef7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Fri, 16 Feb 2018 20:55:57 +0900
Subject: [PATCH 165/286] wip

---
 src/web/app/common/-tags/file-type-icon.tag     | 10 ----------
 .../common/views/components/file-type-icon.vue  | 17 +++++++++++++++++
 2 files changed, 17 insertions(+), 10 deletions(-)
 delete mode 100644 src/web/app/common/-tags/file-type-icon.tag
 create mode 100644 src/web/app/common/views/components/file-type-icon.vue

diff --git a/src/web/app/common/-tags/file-type-icon.tag b/src/web/app/common/-tags/file-type-icon.tag
deleted file mode 100644
index f630efe11..000000000
--- a/src/web/app/common/-tags/file-type-icon.tag
+++ /dev/null
@@ -1,10 +0,0 @@
-<mk-file-type-icon>
-	<template v-if="kind == 'image'">%fa:file-image%</template>
-	<style lang="stylus" scoped>
-		:scope
-			display inline
-	</style>
-	<script lang="typescript">
-		this.kind = this.opts.type.split('/')[0];
-	</script>
-</mk-file-type-icon>
diff --git a/src/web/app/common/views/components/file-type-icon.vue b/src/web/app/common/views/components/file-type-icon.vue
new file mode 100644
index 000000000..aa2f0ed51
--- /dev/null
+++ b/src/web/app/common/views/components/file-type-icon.vue
@@ -0,0 +1,17 @@
+<template>
+<span>
+	<template v-if="kind == 'image'">%fa:file-image%</template>
+</span>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['type'],
+	computed: {
+		kind(): string {
+			return this.type.split('/')[0];
+		}
+	}
+});
+</script>

From 7a8d9e3c2ad309e5f6e50d887f9fe119b7d638fb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 00:36:28 +0900
Subject: [PATCH 166/286] wip

---
 .../drive.tag => views/components/drive.vue}  | 581 +++++++++---------
 1 file changed, 287 insertions(+), 294 deletions(-)
 rename src/web/app/mobile/{tags/drive.tag => views/components/drive.vue} (54%)

diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/views/components/drive.vue
similarity index 54%
rename from src/web/app/mobile/tags/drive.tag
rename to src/web/app/mobile/views/components/drive.vue
index e0a5872d8..a3dd95973 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/views/components/drive.vue
@@ -1,41 +1,38 @@
-<mk-drive>
+<template>
+<div class="mk-drive">
 	<nav ref="nav">
-		<a @click="goRoot" href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</a>
-		<template each={ folder in hierarchyFolders }>
-			<span>%fa:angle-right%</span>
-			<a @click="move" href="/i/drive/folder/{ folder.id }">{ folder.name }</a>
+		<a @click.prevent="goRoot" href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</a>
+		<template v-for="folder in hierarchyFolders">
+			<span :key="folder.id + '>'">%fa:angle-right%</span>
+			<a :key="folder.id" @click.prevent="cd(folder)" :href="`/i/drive/folder/${folder.id}`">{{ folder.name }}</a>
 		</template>
 		<template v-if="folder != null">
 			<span>%fa:angle-right%</span>
-			<p>{ folder.name }</p>
+			<p>{{ folder.name }}</p>
 		</template>
 		<template v-if="file != null">
 			<span>%fa:angle-right%</span>
-			<p>{ file.name }</p>
+			<p>{{ file.name }}</p>
 		</template>
 	</nav>
 	<mk-uploader ref="uploader"/>
-	<div class="browser { fetching: fetching }" v-if="file == null">
+	<div class="browser" :class="{ fetching }" v-if="file == null">
 		<div class="info" v-if="info">
-			<p v-if="folder == null">{ (info.usage / info.capacity * 100).toFixed(1) }% %i18n:mobile.tags.mk-drive.used%</p>
+			<p v-if="folder == null">{{ (info.usage / info.capacity * 100).toFixed(1) }}% %i18n:mobile.tags.mk-drive.used%</p>
 			<p v-if="folder != null && (folder.folders_count > 0 || folder.files_count > 0)">
-				<template v-if="folder.folders_count > 0">{ folder.folders_count } %i18n:mobile.tags.mk-drive.folder-count%</template>
+				<template v-if="folder.folders_count > 0">{{ folder.folders_count }} %i18n:mobile.tags.mk-drive.folder-count%</template>
 				<template v-if="folder.folders_count > 0 && folder.files_count > 0">%i18n:mobile.tags.mk-drive.count-separator%</template>
-				<template v-if="folder.files_count > 0">{ folder.files_count } %i18n:mobile.tags.mk-drive.file-count%</template>
+				<template v-if="folder.files_count > 0">{{ folder.files_count }} %i18n:mobile.tags.mk-drive.file-count%</template>
 			</p>
 		</div>
 		<div class="folders" v-if="folders.length > 0">
-			<template each={ folder in folders }>
-				<mk-drive-folder folder={ folder }/>
-			</template>
+			<mk-drive-folder v-for="folder in folders" :key="folder.id" :folder="folder"/>
 			<p v-if="moreFolders">%i18n:mobile.tags.mk-drive.load-more%</p>
 		</div>
 		<div class="files" v-if="files.length > 0">
-			<template each={ file in files }>
-				<mk-drive-file file={ file }/>
-			</template>
+			<mk-drive-file v-for="file in files" :key="file.id" :file="file"/>
 			<button class="more" v-if="moreFiles" @click="fetchMoreFiles">
-				{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-drive.load-more%' }
+				{{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-drive.load-more%' }}
 			</button>
 		</div>
 		<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
@@ -49,221 +46,117 @@
 			<div class="dot2"></div>
 		</div>
 	</div>
-	<input ref="file" type="file" multiple="multiple" onchange={ changeLocalFile }/>
-	<mk-drive-file-viewer v-if="file != null" file={ file }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
+	<input ref="file" type="file" multiple="multiple" @change="onChangeLocalFile"/>
+	<mk-drive-file-viewer v-if="file != null" :file="file"/>
+</div>
+</template>
 
-			> nav
-				display block
-				position sticky
-				position -webkit-sticky
-				top 0
-				z-index 1
-				width 100%
-				padding 10px 12px
-				overflow auto
-				white-space nowrap
-				font-size 0.9em
-				color rgba(0, 0, 0, 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)
+<script lang="ts">
+import Vue from 'vue';
 
-				> p
-				> a
-					display inline
-					margin 0
-					padding 0
-					text-decoration none !important
-					color inherit
+export default Vue.extend({
+	props: ['initFolder', 'initFile', 'selectFile', 'multiple', 'isNaked', 'top'],
+	data() {
+		return {
+			/**
+			 * 現在の階層(フォルダ)
+			 * * null でルートを表す
+			 */
+			folder: null,
 
-					&:last-child
-						font-weight bold
+			file: null,
 
-					> [data-fa]
-						margin-right 4px
+			files: [],
+			folders: [],
+			moreFiles: false,
+			moreFolders: false,
+			hierarchyFolders: [],
+			selectedFiles: [],
+			info: null,
+			connection: null,
+			connectionId: null,
 
-				> span
-					margin 0 8px
-					opacity 0.5
-
-			> .browser
-				&.fetching
-					opacity 0.5
-
-				> .info
-					border-bottom solid 1px #eee
-
-					&:empty
-						display none
-
-					> p
-						display block
-						max-width 500px
-						margin 0 auto
-						padding 4px 16px
-						font-size 10px
-						color #777
-
-				> .folders
-					> mk-drive-folder
-						border-bottom solid 1px #eee
-
-				> .files
-					> mk-drive-file
-						border-bottom solid 1px #eee
-
-					> .more
-						display block
-						width 100%
-						padding 16px
-						font-size 16px
-						color #555
-
-				> .empty
-					padding 16px
-					text-align center
-					color #999
-					pointer-events none
-
-					> p
-						margin 0
-
-			> .fetching
-				.spinner
-					margin 100px auto
-					width 40px
-					height 40px
-					text-align center
-
-					animation sk-rotate 2.0s infinite linear
-
-				.dot1, .dot2
-					width 60%
-					height 60%
-					display inline-block
-					position absolute
-					top 0
-					background rgba(0, 0, 0, 0.2)
-					border-radius 100%
-
-					animation sk-bounce 2.0s infinite ease-in-out
-
-				.dot2
-					top auto
-					bottom 0
-					animation-delay -1.0s
-
-				@keyframes sk-rotate { 100% { transform: rotate(360deg); }}
-
-				@keyframes sk-bounce {
-					0%, 100% {
-						transform: scale(0.0);
-					} 50% {
-						transform: scale(1.0);
-					}
-				}
-
-			> [ref='file']
-				display none
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-		this.mixin('api');
-
-		this.mixin('drive-stream');
-		this.connection = this.driveStream.getConnection();
-		this.connectionId = this.driveStream.use();
-
-		this.files = [];
-		this.folders = [];
-		this.hierarchyFolders = [];
-		this.selectedFiles = [];
-
-		// 現在の階層(フォルダ)
-		// * null でルートを表す
-		this.folder = null;
-
-		this.file = null;
-
-		this.isFileSelectMode = this.opts.selectFile;
-		this.multiple = this.opts.multiple;
-
-		this.on('mount', () => {
-			this.connection.on('file_created', this.onStreamDriveFileCreated);
-			this.connection.on('file_updated', this.onStreamDriveFileUpdated);
-			this.connection.on('folder_created', this.onStreamDriveFolderCreated);
-			this.connection.on('folder_updated', this.onStreamDriveFolderUpdated);
-
-			if (this.opts.folder) {
-				this.cd(this.opts.folder, true);
-			} else if (this.opts.file) {
-				this.cf(this.opts.file, true);
-			} else {
-				this.fetch();
-			}
-
-			if (this.opts.isNaked) {
-				this.$refs.nav.style.top = `${this.opts.top}px`;
-			}
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('file_created', this.onStreamDriveFileCreated);
-			this.connection.off('file_updated', this.onStreamDriveFileUpdated);
-			this.connection.off('folder_created', this.onStreamDriveFolderCreated);
-			this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
-			this.driveStream.dispose(this.connectionId);
-		});
-
-		this.onStreamDriveFileCreated = file => {
-			this.addFile(file, true);
+			fetching: true,
+			fetchingMoreFiles: false,
+			fetchingMoreFolders: false
 		};
+	},
+	computed: {
+		isFileSelectMode(): boolean {
+			return this.selectFile;
+		}
+	},
+	mounted() {
+		this.connection = this.$root.$data.os.streams.driveStream.getConnection();
+		this.connectionId = this.$root.$data.os.streams.driveStream.use();
 
-		this.onStreamDriveFileUpdated = file => {
+		this.connection.on('file_created', this.onStreamDriveFileCreated);
+		this.connection.on('file_updated', this.onStreamDriveFileUpdated);
+		this.connection.on('folder_created', this.onStreamDriveFolderCreated);
+		this.connection.on('folder_updated', this.onStreamDriveFolderUpdated);
+
+		if (this.initFolder) {
+			this.cd(this.initFolder, true);
+		} else if (this.initFile) {
+			this.cf(this.initFile, true);
+		} else {
+			this.fetch();
+		}
+
+		if (this.isNaked) {
+			(this.$refs.nav as any).style.top = `${this.top}px`;
+		}
+	},
+	beforeDestroy() {
+		this.connection.off('file_created', this.onStreamDriveFileCreated);
+		this.connection.off('file_updated', this.onStreamDriveFileUpdated);
+		this.connection.off('folder_created', this.onStreamDriveFolderCreated);
+		this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
+		this.$root.$data.os.streams.driveStream.dispose(this.connectionId);
+	},
+	methods: {
+		onStreamDriveFileCreated(file) {
+			this.addFile(file, true);
+		},
+
+		onStreamDriveFileUpdated(file) {
 			const current = this.folder ? this.folder.id : null;
 			if (current != file.folder_id) {
 				this.removeFile(file);
 			} else {
 				this.addFile(file, true);
 			}
-		};
+		},
 
-		this.onStreamDriveFolderCreated = folder => {
+		onStreamDriveFolderCreated(folder) {
 			this.addFolder(folder, true);
-		};
+		},
 
-		this.onStreamDriveFolderUpdated = folder => {
+		onStreamDriveFolderUpdated(folder) {
 			const current = this.folder ? this.folder.id : null;
 			if (current != folder.parent_id) {
 				this.removeFolder(folder);
 			} else {
 				this.addFolder(folder, true);
 			}
-		};
+		},
 
-		this.move = ev => {
-			ev.preventDefault();
-			this.cd(ev.item.folder);
-			return false;
-		};
+		dive(folder) {
+			this.hierarchyFolders.unshift(folder);
+			if (folder.parent) this.dive(folder.parent);
+		},
 
-		this.cd = (target, silent = false) => {
+		cd(target, silent = false) {
 			this.file = null;
 
 			if (target == null) {
 				this.goRoot();
 				return;
-			} else if (typeof target == 'object') target = target.id;
+			} else if (typeof target == 'object') {
+				target = target.id;
+			}
 
-			this.update({
-				fetching: true
-			});
+			this.fetching = true;
 
 			this.$root.$data.os.api('drive/folders/show', {
 				folder_id: target
@@ -271,15 +164,14 @@
 				this.folder = folder;
 				this.hierarchyFolders = [];
 
-				if (folder.parent) dive(folder.parent);
+				if (folder.parent) this.dive(folder.parent);
 
-				this.update();
 				this.$emit('open-folder', this.folder, silent);
 				this.fetch();
 			});
-		};
+		},
 
-		this.addFolder = (folder, unshift = false) => {
+		addFolder(folder, unshift = false) {
 			const current = this.folder ? this.folder.id : null;
 			// 追加しようとしているフォルダが、今居る階層とは違う階層のものだったら中断
 			if (current != folder.parent_id) return;
@@ -292,19 +184,16 @@
 			} else {
 				this.folders.push(folder);
 			}
+		},
 
-			this.update();
-		};
-
-		this.addFile = (file, unshift = false) => {
+		addFile(file, unshift = false) {
 			const current = this.folder ? this.folder.id : null;
 			// 追加しようとしているファイルが、今居る階層とは違う階層のものだったら中断
 			if (current != file.folder_id) return;
 
 			if (this.files.some(f => f.id == file.id)) {
 				const exist = this.files.map(f => f.id).indexOf(file.id);
-				this.files[exist] = file;
-				this.update();
+				this.files[exist] = file; // TODO
 				return;
 			}
 
@@ -313,51 +202,47 @@
 			} else {
 				this.files.push(file);
 			}
+		},
 
-			this.update();
-		};
-
-		this.removeFolder = folder => {
+		removeFolder(folder) {
 			if (typeof folder == 'object') folder = folder.id;
 			this.folders = this.folders.filter(f => f.id != folder);
-			this.update();
-		};
+		},
 
-		this.removeFile = file => {
+		removeFile(file) {
 			if (typeof file == 'object') file = file.id;
 			this.files = this.files.filter(f => f.id != file);
-			this.update();
-		};
+		},
 
-		this.appendFile = file => this.addFile(file);
-		this.appendFolder = file => this.addFolder(file);
-		this.prependFile = file => this.addFile(file, true);
-		this.prependFolder = file => this.addFolder(file, true);
-
-		this.goRoot = ev => {
-			ev.preventDefault();
+		appendFile(file) {
+			this.addFile(file);
+		},
+		appendFolder(folder) {
+			this.addFolder(folder);
+		},
+		prependFile(file) {
+			this.addFile(file, true);
+		},
+		prependFolder(folder) {
+			this.addFolder(folder, true);
+		},
 
+		goRoot() {
 			if (this.folder || this.file) {
-				this.update({
-					file: null,
-					folder: null,
-					hierarchyFolders: []
-				});
+				this.file = null;
+				this.folder = null;
+				this.hierarchyFolders = [];
 				this.$emit('move-root');
 				this.fetch();
 			}
+		},
 
-			return false;
-		};
-
-		this.fetch = () => {
-			this.update({
-				folders: [],
-				files: [],
-				moreFolders: false,
-				moreFiles: false,
-				fetching: true
-			});
+		fetch() {
+			this.folders = [];
+			this.files = [];
+			this.moreFolders = false;
+			this.moreFiles = false;
+			this.fetching = true;
 
 			this.$emit('begin-fetch');
 
@@ -398,9 +283,8 @@
 				if (flag) {
 					fetchedFolders.forEach(this.appendFolder);
 					fetchedFiles.forEach(this.appendFile);
-					this.update({
-						fetching: false
-					});
+					this.fetching = false;
+
 					// 一連の読み込みが完了したイベントを発行
 					this.$emit('fetched');
 				} else {
@@ -413,16 +297,14 @@
 			if (this.folder == null) {
 				// Fetch addtional drive info
 				this.$root.$data.os.api('drive').then(info => {
-					this.update({ info });
+					this.info = info;
 				});
 			}
-		};
+		},
 
-		this.fetchMoreFiles = () => {
-			this.update({
-				fetching: true,
-				fetchingMoreFiles: true
-			});
+		fetchMoreFiles() {
+			this.fetching = true;
+			this.fetchingMoreFiles = true;
 
 			const max = 30;
 
@@ -439,14 +321,12 @@
 					this.moreFiles = false;
 				}
 				files.forEach(this.appendFile);
-				this.update({
-					fetching: false,
-					fetchingMoreFiles: false
-				});
+				this.fetching = false;
+				this.fetchingMoreFiles = false;
 			});
-		};
+		},
 
-		this.chooseFile = file => {
+		chooseFile(file) {
 			if (this.isFileSelectMode) {
 				if (this.multiple) {
 					if (this.selectedFiles.some(f => f.id == file.id)) {
@@ -454,7 +334,6 @@
 					} else {
 						this.selectedFiles.push(file);
 					}
-					this.update();
 					this.$emit('change-selection', this.selectedFiles);
 				} else {
 					this.$emit('selected', file);
@@ -462,14 +341,12 @@
 			} else {
 				this.cf(file);
 			}
-		};
+		},
 
-		this.cf = (file, silent = false) => {
+		cf(file, silent = false) {
 			if (typeof file == 'object') file = file.id;
 
-			this.update({
-				fetching: true
-			});
+			this.fetching = true;
 
 			this.$root.$data.os.api('drive/files/show', {
 				file_id: file
@@ -479,19 +356,13 @@
 				this.folder = null;
 				this.hierarchyFolders = [];
 
-				if (file.folder) dive(file.folder);
+				if (file.folder) this.dive(file.folder);
 
-				this.update();
 				this.$emit('open-file', this.file, silent);
 			});
-		};
+		},
 
-		const dive = folder => {
-			this.hierarchyFolders.unshift(folder);
-			if (folder.parent) dive(folder.parent);
-		};
-
-		this.openContextMenu = () => {
+		openContextMenu() {
 			const fn = window.prompt('何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>');
 			if (fn == null || fn == '') return;
 			switch (fn) {
@@ -514,13 +385,13 @@
 					alert('ごめんなさい!フォルダの削除は未実装です...。');
 					break;
 			}
-		};
+		},
 
-		this.selectLocalFile = () => {
-			this.$refs.file.click();
-		};
+		selectLocalFile() {
+			(this.$refs.file as any).click();
+		},
 
-		this.createFolder = () => {
+		createFolder() {
 			const name = window.prompt('フォルダー名');
 			if (name == null || name == '') return;
 			this.$root.$data.os.api('drive/folders/create', {
@@ -528,11 +399,10 @@
 				parent_id: this.folder ? this.folder.id : undefined
 			}).then(folder => {
 				this.addFolder(folder, true);
-				this.update();
 			});
-		};
+		},
 
-		this.renameFolder = () => {
+		renameFolder() {
 			if (this.folder == null) {
 				alert('現在いる場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。');
 				return;
@@ -545,9 +415,9 @@
 			}).then(folder => {
 				this.cd(folder);
 			});
-		};
+		},
 
-		this.moveFolder = () => {
+		moveFolder() {
 			if (this.folder == null) {
 				alert('現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。');
 				return;
@@ -561,9 +431,9 @@
 					this.cd(folder);
 				});
 			});
-		};
+		},
 
-		this.urlUpload = () => {
+		urlUpload() {
 			const url = window.prompt('アップロードしたいファイルのURL');
 			if (url == null || url == '') return;
 			this.$root.$data.os.api('drive/files/upload_from_url', {
@@ -571,10 +441,133 @@
 				folder_id: this.folder ? this.folder.id : undefined
 			});
 			alert('アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。');
-		};
+		},
 
-		this.changeLocalFile = () => {
-			Array.from(this.$refs.file.files).forEach(f => this.$refs.uploader.upload(f, this.folder));
-		};
-	</script>
-</mk-drive>
+		onChangeLocalFile() {
+			Array.from((this.$refs.file as any).files)
+				.forEach(f => (this.$refs.uploader as any).upload(f, this.folder));
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-drive
+	background #fff
+
+	> nav
+		display block
+		position sticky
+		position -webkit-sticky
+		top 0
+		z-index 1
+		width 100%
+		padding 10px 12px
+		overflow auto
+		white-space nowrap
+		font-size 0.9em
+		color rgba(0, 0, 0, 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)
+
+		> p
+		> a
+			display inline
+			margin 0
+			padding 0
+			text-decoration none !important
+			color inherit
+
+			&:last-child
+				font-weight bold
+
+			> [data-fa]
+				margin-right 4px
+
+		> span
+			margin 0 8px
+			opacity 0.5
+
+	> .browser
+		&.fetching
+			opacity 0.5
+
+		> .info
+			border-bottom solid 1px #eee
+
+			&:empty
+				display none
+
+			> p
+				display block
+				max-width 500px
+				margin 0 auto
+				padding 4px 16px
+				font-size 10px
+				color #777
+
+		> .folders
+			> mk-drive-folder
+				border-bottom solid 1px #eee
+
+		> .files
+			> mk-drive-file
+				border-bottom solid 1px #eee
+
+			> .more
+				display block
+				width 100%
+				padding 16px
+				font-size 16px
+				color #555
+
+		> .empty
+			padding 16px
+			text-align center
+			color #999
+			pointer-events none
+
+			> p
+				margin 0
+
+	> .fetching
+		.spinner
+			margin 100px auto
+			width 40px
+			height 40px
+			text-align center
+
+			animation sk-rotate 2.0s infinite linear
+
+		.dot1, .dot2
+			width 60%
+			height 60%
+			display inline-block
+			position absolute
+			top 0
+			background rgba(0, 0, 0, 0.2)
+			border-radius 100%
+
+			animation sk-bounce 2.0s infinite ease-in-out
+
+		.dot2
+			top auto
+			bottom 0
+			animation-delay -1.0s
+
+		@keyframes sk-rotate { 100% { transform: rotate(360deg); }}
+
+		@keyframes sk-bounce {
+			0%, 100% {
+				transform: scale(0.0);
+			} 50% {
+				transform: scale(1.0);
+			}
+		}
+
+	> [ref='file']
+		display none
+
+</style>

From a571ebfc8293d84eaeabb9ea84686c07ce72764a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 01:30:11 +0900
Subject: [PATCH 167/286] wip

---
 .../app/mobile/tags/drive-folder-selector.tag | 69 -------------
 src/web/app/mobile/tags/drive-selector.tag    | 88 -----------------
 .../views/components/drive-file-chooser.vue   | 99 +++++++++++++++++++
 .../views/components/drive-folder-chooser.vue | 79 +++++++++++++++
 4 files changed, 178 insertions(+), 157 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/drive-folder-selector.tag
 delete mode 100644 src/web/app/mobile/tags/drive-selector.tag
 create mode 100644 src/web/app/mobile/views/components/drive-file-chooser.vue
 create mode 100644 src/web/app/mobile/views/components/drive-folder-chooser.vue

diff --git a/src/web/app/mobile/tags/drive-folder-selector.tag b/src/web/app/mobile/tags/drive-folder-selector.tag
deleted file mode 100644
index 7dca527d6..000000000
--- a/src/web/app/mobile/tags/drive-folder-selector.tag
+++ /dev/null
@@ -1,69 +0,0 @@
-<mk-drive-folder-selector>
-	<div class="body">
-		<header>
-			<h1>%i18n:mobile.tags.mk-drive-folder-selector.select-folder%</h1>
-			<button class="close" @click="cancel">%fa:times%</button>
-			<button class="ok" @click="ok">%fa:check%</button>
-		</header>
-		<mk-drive ref="browser" select-folder={ true }/>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			position fixed
-			z-index 2048
-			top 0
-			left 0
-			width 100%
-			height 100%
-			padding 8px
-			background rgba(0, 0, 0, 0.2)
-
-			> .body
-				width 100%
-				height 100%
-				background #fff
-
-				> header
-					border-bottom solid 1px #eee
-
-					> h1
-						margin 0
-						padding 0
-						text-align center
-						line-height 42px
-						font-size 1em
-						font-weight normal
-
-					> .close
-						position absolute
-						top 0
-						left 0
-						line-height 42px
-						width 42px
-
-					> .ok
-						position absolute
-						top 0
-						right 0
-						line-height 42px
-						width 42px
-
-				> mk-drive
-					height calc(100% - 42px)
-					overflow scroll
-					-webkit-overflow-scrolling touch
-
-	</style>
-	<script lang="typescript">
-		this.cancel = () => {
-			this.$emit('canceled');
-			this.$destroy();
-		};
-
-		this.ok = () => {
-			this.$emit('selected', this.$refs.browser.folder);
-			this.$destroy();
-		};
-	</script>
-</mk-drive-folder-selector>
diff --git a/src/web/app/mobile/tags/drive-selector.tag b/src/web/app/mobile/tags/drive-selector.tag
deleted file mode 100644
index 4589592a7..000000000
--- a/src/web/app/mobile/tags/drive-selector.tag
+++ /dev/null
@@ -1,88 +0,0 @@
-<mk-drive-selector>
-	<div class="body">
-		<header>
-			<h1>%i18n:mobile.tags.mk-drive-selector.select-file%<span class="count" v-if="files.length > 0">({ files.length })</span></h1>
-			<button class="close" @click="cancel">%fa:times%</button>
-			<button v-if="opts.multiple" class="ok" @click="ok">%fa:check%</button>
-		</header>
-		<mk-drive ref="browser" select-file={ true } multiple={ opts.multiple }/>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			position fixed
-			z-index 2048
-			top 0
-			left 0
-			width 100%
-			height 100%
-			padding 8px
-			background rgba(0, 0, 0, 0.2)
-
-			> .body
-				width 100%
-				height 100%
-				background #fff
-
-				> header
-					border-bottom solid 1px #eee
-
-					> h1
-						margin 0
-						padding 0
-						text-align center
-						line-height 42px
-						font-size 1em
-						font-weight normal
-
-						> .count
-							margin-left 4px
-							opacity 0.5
-
-					> .close
-						position absolute
-						top 0
-						left 0
-						line-height 42px
-						width 42px
-
-					> .ok
-						position absolute
-						top 0
-						right 0
-						line-height 42px
-						width 42px
-
-				> mk-drive
-					height calc(100% - 42px)
-					overflow scroll
-					-webkit-overflow-scrolling touch
-
-	</style>
-	<script lang="typescript">
-		this.files = [];
-
-		this.on('mount', () => {
-			this.$refs.browser.on('change-selection', files => {
-				this.update({
-					files: files
-				});
-			});
-
-			this.$refs.browser.on('selected', file => {
-				this.$emit('selected', file);
-				this.$destroy();
-			});
-		});
-
-		this.cancel = () => {
-			this.$emit('canceled');
-			this.$destroy();
-		};
-
-		this.ok = () => {
-			this.$emit('selected', this.files);
-			this.$destroy();
-		};
-	</script>
-</mk-drive-selector>
diff --git a/src/web/app/mobile/views/components/drive-file-chooser.vue b/src/web/app/mobile/views/components/drive-file-chooser.vue
new file mode 100644
index 000000000..4071636a7
--- /dev/null
+++ b/src/web/app/mobile/views/components/drive-file-chooser.vue
@@ -0,0 +1,99 @@
+<template>
+<div class="mk-drive-file-chooser">
+	<div class="body">
+		<header>
+			<h1>%i18n:mobile.tags.mk-drive-selector.select-file%<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1>
+			<button class="close" @click="cancel">%fa:times%</button>
+			<button v-if="opts.multiple" class="ok" @click="ok">%fa:check%</button>
+		</header>
+		<mk-drive ref="browser"
+			select-file
+			:multiple="multiple"
+			@change-selection="onChangeSelection"
+			@selected="onSelected"
+		/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['multiple'],
+	data() {
+		return {
+			files: []
+		};
+	},
+	methods: {
+		onChangeSelection(files) {
+			this.files = files;
+		},
+		onSelected(file) {
+			this.$emit('selected', file);
+			this.$destroy();
+		},
+		cancel() {
+			this.$emit('canceled');
+			this.$destroy();
+		},
+		ok() {
+			this.$emit('selected', this.files);
+			this.$destroy();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-drive-file-chooser
+	display block
+	position fixed
+	z-index 2048
+	top 0
+	left 0
+	width 100%
+	height 100%
+	padding 8px
+	background rgba(0, 0, 0, 0.2)
+
+	> .body
+		width 100%
+		height 100%
+		background #fff
+
+		> header
+			border-bottom solid 1px #eee
+
+			> h1
+				margin 0
+				padding 0
+				text-align center
+				line-height 42px
+				font-size 1em
+				font-weight normal
+
+				> .count
+					margin-left 4px
+					opacity 0.5
+
+			> .close
+				position absolute
+				top 0
+				left 0
+				line-height 42px
+				width 42px
+
+			> .ok
+				position absolute
+				top 0
+				right 0
+				line-height 42px
+				width 42px
+
+		> .mk-drive
+			height calc(100% - 42px)
+			overflow scroll
+			-webkit-overflow-scrolling touch
+
+</style>
diff --git a/src/web/app/mobile/views/components/drive-folder-chooser.vue b/src/web/app/mobile/views/components/drive-folder-chooser.vue
new file mode 100644
index 000000000..ebf0a6c4b
--- /dev/null
+++ b/src/web/app/mobile/views/components/drive-folder-chooser.vue
@@ -0,0 +1,79 @@
+<template>
+<div class="mk-drive-folder-chooser">
+	<div class="body">
+		<header>
+			<h1>%i18n:mobile.tags.mk-drive-folder-selector.select-folder%</h1>
+			<button class="close" @click="cancel">%fa:times%</button>
+			<button v-if="opts.multiple" class="ok" @click="ok">%fa:check%</button>
+		</header>
+		<mk-drive ref="browser"
+			select-folder
+		/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	methods: {
+		cancel() {
+			this.$emit('canceled');
+			this.$destroy();
+		},
+		ok() {
+			this.$emit('selected', (this.$refs.browser as any).folder);
+			this.$destroy();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-drive-folder-chooser
+	display block
+	position fixed
+	z-index 2048
+	top 0
+	left 0
+	width 100%
+	height 100%
+	padding 8px
+	background rgba(0, 0, 0, 0.2)
+
+	> .body
+		width 100%
+		height 100%
+		background #fff
+
+		> header
+			border-bottom solid 1px #eee
+
+			> h1
+				margin 0
+				padding 0
+				text-align center
+				line-height 42px
+				font-size 1em
+				font-weight normal
+
+			> .close
+				position absolute
+				top 0
+				left 0
+				line-height 42px
+				width 42px
+
+			> .ok
+				position absolute
+				top 0
+				right 0
+				line-height 42px
+				width 42px
+
+		> .mk-drive
+			height calc(100% - 42px)
+			overflow scroll
+			-webkit-overflow-scrolling touch
+
+</style>

From fe1d6a1b09972df918873f86efcd9d962e8f2dec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 01:30:56 +0900
Subject: [PATCH 168/286] wip

---
 src/web/app/mobile/views/components/drive-file-chooser.vue   | 1 -
 src/web/app/mobile/views/components/drive-folder-chooser.vue | 1 -
 2 files changed, 2 deletions(-)

diff --git a/src/web/app/mobile/views/components/drive-file-chooser.vue b/src/web/app/mobile/views/components/drive-file-chooser.vue
index 4071636a7..6f1d25f63 100644
--- a/src/web/app/mobile/views/components/drive-file-chooser.vue
+++ b/src/web/app/mobile/views/components/drive-file-chooser.vue
@@ -47,7 +47,6 @@ export default Vue.extend({
 
 <style lang="stylus" scoped>
 .mk-drive-file-chooser
-	display block
 	position fixed
 	z-index 2048
 	top 0
diff --git a/src/web/app/mobile/views/components/drive-folder-chooser.vue b/src/web/app/mobile/views/components/drive-folder-chooser.vue
index ebf0a6c4b..53cc67c6c 100644
--- a/src/web/app/mobile/views/components/drive-folder-chooser.vue
+++ b/src/web/app/mobile/views/components/drive-folder-chooser.vue
@@ -31,7 +31,6 @@ export default Vue.extend({
 
 <style lang="stylus" scoped>
 .mk-drive-folder-chooser
-	display block
 	position fixed
 	z-index 2048
 	top 0

From 7d377925a02a785966ca22f9f9be085a6ee808c1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 02:24:10 +0900
Subject: [PATCH 169/286] wip

---
 src/web/app/common/views/components/index.ts  |  2 +
 .../views/components/special-message.vue      |  4 +-
 .../views/components/widgets/profile.vue      |  2 +-
 src/web/app/desktop/script.ts                 |  3 +
 src/web/app/desktop/views/components/index.ts |  2 +
 .../views/components/notifications.vue        | 28 ++++-----
 .../desktop/views/components/post-preview.vue |  4 +-
 .../views/components/posts-post-sub.vue       |  4 +-
 .../desktop/views/components/posts-post.vue   |  8 +--
 .../desktop/views/components/user-preview.vue |  4 +-
 src/web/app/desktop/views/directives/index.ts |  6 ++
 .../desktop/views/directives/user-preview.ts  | 63 +++++++++++++++++++
 src/web/app/mobile/script.ts                  |  3 +
 src/web/app/mobile/views/directives/index.ts  |  6 ++
 .../mobile/views/directives/user-preview.ts   |  2 +
 15 files changed, 115 insertions(+), 26 deletions(-)
 create mode 100644 src/web/app/desktop/views/directives/index.ts
 create mode 100644 src/web/app/desktop/views/directives/user-preview.ts
 create mode 100644 src/web/app/mobile/views/directives/index.ts
 create mode 100644 src/web/app/mobile/views/directives/user-preview.ts

diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 452621756..10d09ce65 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -10,6 +10,7 @@ import reactionsViewer from './reactions-viewer.vue';
 import time from './time.vue';
 import images from './images.vue';
 import uploader from './uploader.vue';
+import specialMessage from './special-message.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
@@ -21,3 +22,4 @@ Vue.component('mk-reactions-viewer', reactionsViewer);
 Vue.component('mk-time', time);
 Vue.component('mk-images', images);
 Vue.component('mk-uploader', uploader);
+Vue.component('mk-special-message', specialMessage);
diff --git a/src/web/app/common/views/components/special-message.vue b/src/web/app/common/views/components/special-message.vue
index 900afe178..2fd4d6515 100644
--- a/src/web/app/common/views/components/special-message.vue
+++ b/src/web/app/common/views/components/special-message.vue
@@ -15,10 +15,10 @@ export default Vue.extend({
 	},
 	computed: {
 		d(): number {
-			return now.getDate();
+			return this.now.getDate();
 		},
 		m(): number {
-			return now.getMonth() + 1;
+			return this.now.getMonth() + 1;
 		}
 	}
 });
diff --git a/src/web/app/common/views/components/widgets/profile.vue b/src/web/app/common/views/components/widgets/profile.vue
index 70902c7cf..d64ffad93 100644
--- a/src/web/app/common/views/components/widgets/profile.vue
+++ b/src/web/app/common/views/components/widgets/profile.vue
@@ -13,7 +13,7 @@
 		@click="wapi_setAvatar"
 		alt="avatar"
 		title="クリックでアバター編集"
-		:v-user-preview={ I.id }
+		v-user-preview={ I.id }
 	/>
 	<a class="name" href={ '/' + I.username }>{ I.name }</a>
 	<p class="username">@{ I.username }</p>
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index d6ad0202d..1377965ea 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -21,6 +21,9 @@ init(async (launch) => {
 	 */
 	fuckAdBlock();
 
+	// Register directives
+	require('./views/directives');
+
 	// Register components
 	require('./views/components');
 
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 6b58215be..7a7438214 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -25,6 +25,7 @@ import imagesImageDialog from './images-image-dialog.vue';
 import notifications from './notifications.vue';
 import postForm from './post-form.vue';
 import repostForm from './repost-form.vue';
+import followButton from './follow-button.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-header', uiHeader);
@@ -51,3 +52,4 @@ Vue.component('mk-images-image-dialog', imagesImageDialog);
 Vue.component('mk-notifications', notifications);
 Vue.component('mk-post-form', postForm);
 Vue.component('mk-repost-form', repostForm);
+Vue.component('mk-follow-button', followButton);
diff --git a/src/web/app/desktop/views/components/notifications.vue b/src/web/app/desktop/views/components/notifications.vue
index 5826fc210..d211a933f 100644
--- a/src/web/app/desktop/views/components/notifications.vue
+++ b/src/web/app/desktop/views/components/notifications.vue
@@ -5,13 +5,13 @@
 			<div class="notification" :class="notification.type" :key="notification.id">
 				<mk-time :time="notification.created_at"/>
 				<template v-if="notification.type == 'reaction'">
-					<a class="avatar-anchor" :href="`/${notification.user.username}`" :v-user-preview="notification.user.id">
+					<a class="avatar-anchor" :href="`/${notification.user.username}`" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</a>
 					<div class="text">
 						<p>
 							<mk-reaction-icon reaction={ notification.reaction }/>
-							<a :href="`/${notification.user.username}`" :v-user-preview="notification.user.id">{{ notification.user.name }}</a>
+							<a :href="`/${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a>
 						</p>
 						<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
@@ -19,12 +19,12 @@
 					</div>
 				</template>
 				<template v-if="notification.type == 'repost'">
-					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">
+					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">
 						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</a>
 					<div class="text">
 						<p>%fa:retweet%
-							<a :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
+							<a :href="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
 						</p>
 						<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%
@@ -32,54 +32,54 @@
 					</div>
 				</template>
 				<template v-if="notification.type == 'quote'">
-					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">
+					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">
 						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</a>
 					<div class="text">
 						<p>%fa:quote-left%
-							<a :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
+							<a :href="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
 						</p>
 						<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
 					</div>
 				</template>
 				<template v-if="notification.type == 'follow'">
-					<a class="avatar-anchor" :href="`/${notification.user.username}`" :v-user-preview="notification.user.id">
+					<a class="avatar-anchor" :href="`/${notification.user.username}`" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</a>
 					<div class="text">
 						<p>%fa:user-plus%
-							<a :href="`/${notification.user.username}`" :v-user-preview="notification.user.id">{{ notification.user.name }}</a>
+							<a :href="`/${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a>
 						</p>
 					</div>
 				</template>
 				<template v-if="notification.type == 'reply'">
-					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">
+					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">
 						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</a>
 					<div class="text">
 						<p>%fa:reply%
-							<a :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
+							<a :href="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
 						</p>
 						<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
 					</div>
 				</template>
 				<template v-if="notification.type == 'mention'">
-					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">
+					<a class="avatar-anchor" :href="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">
 						<img class="avatar" :src="`${notification.post.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</a>
 					<div class="text">
 						<p>%fa:at%
-							<a :href="`/${notification.post.user.username}`" :v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
+							<a :href="`/${notification.post.user.username}`" v-user-preview="notification.post.user_id">{{ notification.post.user.name }}</a>
 						</p>
 						<a class="post-preview" :href="`/${notification.post.user.username}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a>
 					</div>
 				</template>
 				<template v-if="notification.type == 'poll_vote'">
-					<a class="avatar-anchor" :href="`/${notification.user.username}`" :v-user-preview="notification.user.id">
+					<a class="avatar-anchor" :href="`/${notification.user.username}`" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatar_url}?thumbnail&size=48`" alt="avatar"/>
 					</a>
 					<div class="text">
-						<p>%fa:chart-pie%<a :href="`/${notification.user.username}`" :v-user-preview="notification.user.id">{{ notification.user.name }}</a></p>
+						<p>%fa:chart-pie%<a :href="`/${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a></p>
 						<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
 							%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%
 						</a>
diff --git a/src/web/app/desktop/views/components/post-preview.vue b/src/web/app/desktop/views/components/post-preview.vue
index fc297dccc..f22b28153 100644
--- a/src/web/app/desktop/views/components/post-preview.vue
+++ b/src/web/app/desktop/views/components/post-preview.vue
@@ -1,11 +1,11 @@
 <template>
 <div class="mk-post-preview" :title="title">
 	<a class="avatar-anchor" :href="`/${post.user.username}`">
-		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" :v-user-preview="post.user_id"/>
+		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.user_id"/>
 	</a>
 	<div class="main">
 		<header>
-			<a class="name" :href="`/${post.user.username}`" :v-user-preview="post.user_id">{{ post.user.name }}</a>
+			<a class="name" :href="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</a>
 			<span class="username">@{ post.user.username }</span>
 			<a class="time" :href="`/${post.user.username}/${post.id}`">
 			<mk-time :time="post.created_at"/></a>
diff --git a/src/web/app/desktop/views/components/posts-post-sub.vue b/src/web/app/desktop/views/components/posts-post-sub.vue
index 89aeb0482..cccc24653 100644
--- a/src/web/app/desktop/views/components/posts-post-sub.vue
+++ b/src/web/app/desktop/views/components/posts-post-sub.vue
@@ -1,11 +1,11 @@
 <template>
 <div class="mk-posts-post-sub" :title="title">
 	<a class="avatar-anchor" :href="`/${post.user.username}`">
-		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" :v-user-preview="post.user_id"/>
+		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.user_id"/>
 	</a>
 	<div class="main">
 		<header>
-			<a class="name" :href="`/${post.user.username}`" :v-user-preview="post.user_id">{{ post.user.name }}</a>
+			<a class="name" :href="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</a>
 			<span class="username">@{{ post.user.username }}</span>
 			<a class="created-at" :href="`/${post.user.username}/${post.id}`">
 				<mk-time :time="post.created_at"/>
diff --git a/src/web/app/desktop/views/components/posts-post.vue b/src/web/app/desktop/views/components/posts-post.vue
index 77a1e882c..2a4c39a97 100644
--- a/src/web/app/desktop/views/components/posts-post.vue
+++ b/src/web/app/desktop/views/components/posts-post.vue
@@ -5,20 +5,20 @@
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<a class="avatar-anchor" :href="`/${post.user.username}`" :v-user-preview="post.user_id">
+			<a class="avatar-anchor" :href="`/${post.user.username}`" v-user-preview="post.user_id">
 				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
 			</a>
-			%fa:retweet%{{'%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{'))}}<a class="name" :href="`/${post.user.username}`" :v-user-preview="post.user_id">{{ post.user.name }}</a>{{'%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1)}}
+			%fa:retweet%{{'%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{'))}}<a class="name" :href="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</a>{{'%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1)}}
 		</p>
 		<mk-time :time="post.created_at"/>
 	</div>
 	<article>
 		<a class="avatar-anchor" :href="`/${p.user.username}`">
-			<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=64`" alt="avatar" :v-user-preview="p.user.id"/>
+			<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</a>
 		<div class="main">
 			<header>
-				<a class="name" :href="`/${p.user.username}`" :v-user-preview="p.user.id">{{ p.user.name }}</a>
+				<a class="name" :href="`/${p.user.username}`" v-user-preview="p.user.id">{{ p.user.name }}</a>
 				<span class="is-bot" v-if="p.user.is_bot">bot</span>
 				<span class="username">@{{ p.user.username }}</span>
 				<div class="info">
diff --git a/src/web/app/desktop/views/components/user-preview.vue b/src/web/app/desktop/views/components/user-preview.vue
index fb6ae2553..71b17503b 100644
--- a/src/web/app/desktop/views/components/user-preview.vue
+++ b/src/web/app/desktop/views/components/user-preview.vue
@@ -45,7 +45,9 @@ export default Vue.extend({
 	mounted() {
 		if (typeof this.user == 'object') {
 			this.u = this.user;
-			this.open();
+			this.$nextTick(() => {
+				this.open();
+			});
 		} else {
 			this.$root.$data.os.api('users/show', {
 				user_id: this.user[0] == '@' ? undefined : this.user,
diff --git a/src/web/app/desktop/views/directives/index.ts b/src/web/app/desktop/views/directives/index.ts
new file mode 100644
index 000000000..324e07596
--- /dev/null
+++ b/src/web/app/desktop/views/directives/index.ts
@@ -0,0 +1,6 @@
+import Vue from 'vue';
+
+import userPreview from './user-preview';
+
+Vue.directive('userPreview', userPreview);
+Vue.directive('user-preview', userPreview);
diff --git a/src/web/app/desktop/views/directives/user-preview.ts b/src/web/app/desktop/views/directives/user-preview.ts
new file mode 100644
index 000000000..7d6993667
--- /dev/null
+++ b/src/web/app/desktop/views/directives/user-preview.ts
@@ -0,0 +1,63 @@
+import MkUserPreview from '../components/user-preview.vue';
+
+export default {
+	bind(el, binding, vn) {
+		const self = vn.context._userPreviewDirective_ = {} as any;
+
+		self.user = binding.value;
+
+		let tag = null;
+		self.showTimer = null;
+		self.hideTimer = null;
+
+		self.close = () => {
+			if (tag) {
+				tag.close();
+				tag = null;
+			}
+		};
+
+		const show = () => {
+			if (tag) return;
+			tag = new MkUserPreview({
+				parent: vn.context,
+				propsData: {
+					user: self.user
+				}
+			}).$mount();
+			const preview = tag.$el;
+			const rect = el.getBoundingClientRect();
+			const x = rect.left + el.offsetWidth + window.pageXOffset;
+			const y = rect.top + window.pageYOffset;
+			preview.style.top = y + 'px';
+			preview.style.left = x + 'px';
+			preview.addEventListener('mouseover', () => {
+				clearTimeout(self.hideTimer);
+			});
+			preview.addEventListener('mouseleave', () => {
+				clearTimeout(self.showTimer);
+				self.hideTimer = setTimeout(self.close, 500);
+			});
+			document.body.appendChild(preview);
+		};
+
+		el.addEventListener('mouseover', () => {
+			clearTimeout(self.showTimer);
+			clearTimeout(self.hideTimer);
+			self.showTimer = setTimeout(show, 500);
+		});
+
+		el.addEventListener('mouseleave', () => {
+			clearTimeout(self.showTimer);
+			clearTimeout(self.hideTimer);
+			self.hideTimer = setTimeout(self.close, 500);
+		});
+	},
+	unbind(el, binding, vn) {
+		const self = vn.context._userPreviewDirective_;
+		console.log('unbound:', self.user);
+		clearTimeout(self.showTimer);
+		clearTimeout(self.hideTimer);
+		self.close();
+	}
+};
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index f7129c553..f2d617f3a 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -12,6 +12,9 @@ import init from '../init';
  * init
  */
 init((launch) => {
+	// Register directives
+	require('./views/directives');
+
 	// http://qiita.com/junya/items/3ff380878f26ca447f85
 	document.body.setAttribute('ontouchstart', '');
 
diff --git a/src/web/app/mobile/views/directives/index.ts b/src/web/app/mobile/views/directives/index.ts
new file mode 100644
index 000000000..324e07596
--- /dev/null
+++ b/src/web/app/mobile/views/directives/index.ts
@@ -0,0 +1,6 @@
+import Vue from 'vue';
+
+import userPreview from './user-preview';
+
+Vue.directive('userPreview', userPreview);
+Vue.directive('user-preview', userPreview);
diff --git a/src/web/app/mobile/views/directives/user-preview.ts b/src/web/app/mobile/views/directives/user-preview.ts
new file mode 100644
index 000000000..1a54abc20
--- /dev/null
+++ b/src/web/app/mobile/views/directives/user-preview.ts
@@ -0,0 +1,2 @@
+// nope
+export default {};

From 684662a475685787eb2aa09158bc78a4e80fbf85 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 03:01:00 +0900
Subject: [PATCH 170/286] wip

---
 src/web/app/common/views/components/index.ts  |   2 +
 .../app/common/views/components/messaging.vue |   4 +-
 .../views/components/reactions-viewer.vue     |   2 +-
 .../views/components/stream-indicator.vue     | 126 ++++++++++--------
 .../views/components/widgets/messaging.vue    |   2 +-
 .../views/components/friends-maker.vue        |   2 +-
 src/web/app/desktop/views/components/index.ts |   2 +
 .../desktop/views/components/list-user.vue    |   2 +-
 .../views/components/notifications.vue        |   2 +-
 .../views/components/post-detail-sub.vue      |   2 +-
 .../desktop/views/components/post-detail.vue  |   2 +-
 .../desktop/views/components/posts-post.vue   |  14 +-
 .../desktop/views/components/repost-form.vue  |   2 +-
 .../app/desktop/views/components/timeline.vue |   2 +-
 .../components/ui-header-notifications.vue    |   2 +-
 .../desktop/views/pages/user/user-friends.vue |   2 +-
 .../desktop/views/pages/user/user-home.vue    |   2 +-
 .../desktop/views/pages/user/user-profile.vue |   2 +-
 src/web/app/mobile/views/components/drive.vue |   4 +-
 .../mobile/views/components/friends-maker.vue |   2 +-
 .../mobile/views/components/notification.vue  |   2 +-
 .../mobile/views/components/notifications.vue |   2 +-
 .../app/mobile/views/components/post-card.vue |   2 +-
 .../mobile/views/components/post-detail.vue   |   2 +-
 .../app/mobile/views/components/post-form.vue |   4 +-
 .../mobile/views/components/posts-post.vue    |   8 +-
 .../app/mobile/views/components/user-card.vue |   2 +-
 src/web/app/mobile/views/pages/user.vue       |   4 +-
 .../mobile/views/pages/user/home-friends.vue  |   2 +-
 src/web/app/mobile/views/pages/user/home.vue  |   2 +-
 webpack/webpack.config.ts                     |   3 +-
 31 files changed, 119 insertions(+), 94 deletions(-)

diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 10d09ce65..e3f105f58 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -11,6 +11,7 @@ import time from './time.vue';
 import images from './images.vue';
 import uploader from './uploader.vue';
 import specialMessage from './special-message.vue';
+import streamIndicator from './stream-indicator.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
@@ -23,3 +24,4 @@ Vue.component('mk-time', time);
 Vue.component('mk-images', images);
 Vue.component('mk-uploader', uploader);
 Vue.component('mk-special-message', specialMessage);
+Vue.component('mk-stream-indicator', streamIndicator);
diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index 386e705b0..f45f99b53 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -180,7 +180,7 @@ export default Vue.extend({
 					padding 16px
 
 					> header
-						> mk-time
+						> .mk-time
 							font-size 1em
 
 					> .avatar
@@ -381,7 +381,7 @@ export default Vue.extend({
 						margin 0 0 0 8px
 						color rgba(0, 0, 0, 0.5)
 
-					> mk-time
+					> .mk-time
 						position absolute
 						top 0
 						right 0
diff --git a/src/web/app/common/views/components/reactions-viewer.vue b/src/web/app/common/views/components/reactions-viewer.vue
index 696aef335..f6a27d913 100644
--- a/src/web/app/common/views/components/reactions-viewer.vue
+++ b/src/web/app/common/views/components/reactions-viewer.vue
@@ -38,7 +38,7 @@ export default Vue.extend({
 	> span
 		margin-right 8px
 
-		> mk-reaction-icon
+		> .mk-reaction-icon
 			font-size 1.4em
 
 		> span
diff --git a/src/web/app/common/views/components/stream-indicator.vue b/src/web/app/common/views/components/stream-indicator.vue
index 564376bba..00bd58c1f 100644
--- a/src/web/app/common/views/components/stream-indicator.vue
+++ b/src/web/app/common/views/components/stream-indicator.vue
@@ -1,74 +1,92 @@
 <template>
-	<div>
-		<p v-if=" stream.state == 'initializing' ">
-			%fa:spinner .pulse%
-			<span>%i18n:common.tags.mk-stream-indicator.connecting%<mk-ellipsis/></span>
-		</p>
-		<p v-if=" stream.state == 'reconnecting' ">
-			%fa:spinner .pulse%
-			<span>%i18n:common.tags.mk-stream-indicator.reconnecting%<mk-ellipsis/></span>
-		</p>
-		<p v-if=" stream.state == 'connected' ">
-			%fa:check%
-			<span>%i18n:common.tags.mk-stream-indicator.connected%</span>
-		</p>
-	</div>
+<div class="mk-stream-indicator" v-if="stream">
+	<p v-if=" stream.state == 'initializing' ">
+		%fa:spinner .pulse%
+		<span>%i18n:common.tags.mk-stream-indicator.connecting%<mk-ellipsis/></span>
+	</p>
+	<p v-if=" stream.state == 'reconnecting' ">
+		%fa:spinner .pulse%
+		<span>%i18n:common.tags.mk-stream-indicator.reconnecting%<mk-ellipsis/></span>
+	</p>
+	<p v-if=" stream.state == 'connected' ">
+		%fa:check%
+		<span>%i18n:common.tags.mk-stream-indicator.connected%</span>
+	</p>
+</div>
 </template>
 
-<script lang="typescript">
-	import * as anime from 'animejs';
-	import Ellipsis from './ellipsis.vue';
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
 
-	export default {
-		props: ['stream'],
-		created() {
+export default Vue.extend({
+	data() {
+		return {
+			stream: null
+		};
+	},
+	created() {
+		this.stream = this.$root.$data.os.stream.borrow();
+
+		this.$root.$data.os.stream.on('connected', this.onConnected);
+		this.$root.$data.os.stream.on('disconnected', this.onDisconnected);
+
+		this.$nextTick(() => {
 			if (this.stream.state == 'connected') {
-				this.root.style.opacity = 0;
+				this.$el.style.opacity = '0';
 			}
+		});
+	},
+	beforeDestroy() {
+		this.$root.$data.os.stream.off('connected', this.onConnected);
+		this.$root.$data.os.stream.off('disconnected', this.onDisconnected);
+	},
+	methods: {
+		onConnected() {
+			this.stream = this.$root.$data.os.stream.borrow();
 
-			this.stream.on('_connected_', () => {
-				setTimeout(() => {
-					anime({
-						targets: this.root,
-						opacity: 0,
-						easing: 'linear',
-						duration: 200
-					});
-				}, 1000);
-			});
-
-			this.stream.on('_closed_', () => {
+			setTimeout(() => {
 				anime({
-					targets: this.root,
-					opacity: 1,
+					targets: this.$el,
+					opacity: 0,
 					easing: 'linear',
-					duration: 100
+					duration: 200
 				});
+			}, 1000);
+		},
+		onDisconnected() {
+			this.stream = null;
+
+			anime({
+				targets: this.$el,
+				opacity: 1,
+				easing: 'linear',
+				duration: 100
 			});
 		}
-	};
+	}
+});
 </script>
 
 <style lang="stylus" scoped>
-	> div
+.mk-stream-indicator
+	pointer-events none
+	position fixed
+	z-index 16384
+	bottom 8px
+	right 8px
+	margin 0
+	padding 6px 12px
+	font-size 0.9em
+	color #fff
+	background rgba(0, 0, 0, 0.8)
+	border-radius 4px
+
+	> p
 		display block
-		pointer-events none
-		position fixed
-		z-index 16384
-		bottom 8px
-		right 8px
 		margin 0
-		padding 6px 12px
-		font-size 0.9em
-		color #fff
-		background rgba(0, 0, 0, 0.8)
-		border-radius 4px
 
-		> p
-			display block
-			margin 0
-
-			> [data-fa]
-				margin-right 0.25em
+		> [data-fa]
+			margin-right 0.25em
 
 </style>
diff --git a/src/web/app/common/views/components/widgets/messaging.vue b/src/web/app/common/views/components/widgets/messaging.vue
index 19ef70431..f31acc5c6 100644
--- a/src/web/app/common/views/components/widgets/messaging.vue
+++ b/src/web/app/common/views/components/widgets/messaging.vue
@@ -52,7 +52,7 @@ export default define({
 		> [data-fa]
 			margin-right 4px
 
-	> mk-messaging
+	> .mk-messaging
 		max-height 250px
 		overflow auto
 
diff --git a/src/web/app/desktop/views/components/friends-maker.vue b/src/web/app/desktop/views/components/friends-maker.vue
index add6c10a3..caa5f4913 100644
--- a/src/web/app/desktop/views/components/friends-maker.vue
+++ b/src/web/app/desktop/views/components/friends-maker.vue
@@ -114,7 +114,7 @@ export default Vue.extend({
 					line-height 16px
 					color #ccc
 
-			> mk-follow-button
+			> .mk-follow-button
 				position absolute
 				top 16px
 				right 16px
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 7a7438214..1e4c2bafc 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -26,6 +26,7 @@ import notifications from './notifications.vue';
 import postForm from './post-form.vue';
 import repostForm from './repost-form.vue';
 import followButton from './follow-button.vue';
+import postPreview from './post-preview.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-header', uiHeader);
@@ -53,3 +54,4 @@ Vue.component('mk-notifications', notifications);
 Vue.component('mk-post-form', postForm);
 Vue.component('mk-repost-form', repostForm);
 Vue.component('mk-follow-button', followButton);
+Vue.component('mk-post-preview', postPreview);
diff --git a/src/web/app/desktop/views/components/list-user.vue b/src/web/app/desktop/views/components/list-user.vue
index 28304e475..adaa8f092 100644
--- a/src/web/app/desktop/views/components/list-user.vue
+++ b/src/web/app/desktop/views/components/list-user.vue
@@ -93,7 +93,7 @@ export default Vue.extend({
 				font-size 1.1em
 				color #717171
 
-	> mk-follow-button
+	> .mk-follow-button
 		position absolute
 		top 16px
 		right 16px
diff --git a/src/web/app/desktop/views/components/notifications.vue b/src/web/app/desktop/views/components/notifications.vue
index d211a933f..f19766dc8 100644
--- a/src/web/app/desktop/views/components/notifications.vue
+++ b/src/web/app/desktop/views/components/notifications.vue
@@ -197,7 +197,7 @@ export default Vue.extend({
 			&:last-child
 				border-bottom none
 
-			> mk-time
+			> .mk-time
 				display inline
 				position absolute
 				top 16px
diff --git a/src/web/app/desktop/views/components/post-detail-sub.vue b/src/web/app/desktop/views/components/post-detail-sub.vue
index 42f8be3b1..8d81e6860 100644
--- a/src/web/app/desktop/views/components/post-detail-sub.vue
+++ b/src/web/app/desktop/views/components/post-detail-sub.vue
@@ -119,7 +119,7 @@ export default Vue.extend({
 				font-size 1em
 				color #717171
 
-				> mk-url-preview
+				> .mk-url-preview
 					margin-top 8px
 
 </style>
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index 6c36f06fa..d23043dd4 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -280,7 +280,7 @@ export default Vue.extend({
 				font-size 1.5em
 				color #717171
 
-				> mk-url-preview
+				> .mk-url-preview
 					margin-top 8px
 
 		> footer
diff --git a/src/web/app/desktop/views/components/posts-post.vue b/src/web/app/desktop/views/components/posts-post.vue
index 2a4c39a97..e611b2513 100644
--- a/src/web/app/desktop/views/components/posts-post.vue
+++ b/src/web/app/desktop/views/components/posts-post.vue
@@ -178,6 +178,7 @@ export default Vue.extend({
 		},
 		reply() {
 			document.body.appendChild(new MkPostFormWindow({
+				parent: this,
 				propsData: {
 					reply: this.p
 				}
@@ -185,6 +186,7 @@ export default Vue.extend({
 		},
 		repost() {
 			document.body.appendChild(new MkRepostFormWindow({
+				parent: this,
 				propsData: {
 					post: this.p
 				}
@@ -192,6 +194,7 @@ export default Vue.extend({
 		},
 		react() {
 			document.body.appendChild(new MkReactionPicker({
+				parent: this,
 				propsData: {
 					source: this.$refs.reactButton,
 					post: this.p
@@ -200,6 +203,7 @@ export default Vue.extend({
 		},
 		menu() {
 			document.body.appendChild(new MkPostMenu({
+				parent: this,
 				propsData: {
 					source: this.$refs.menuButton,
 					post: this.p
@@ -303,7 +307,7 @@ export default Vue.extend({
 			.name
 				font-weight bold
 
-		> mk-time
+		> .mk-time
 			position absolute
 			top 16px
 			right 32px
@@ -317,7 +321,7 @@ export default Vue.extend({
 		padding 0 16px
 		background rgba(0, 0, 0, 0.0125)
 
-		> mk-post-preview
+		> .mk-post-preview
 			background transparent
 
 	> article
@@ -415,7 +419,7 @@ export default Vue.extend({
 					> .dummy
 						display none
 
-					mk-url-preview
+					.mk-url-preview
 						margin-top 8px
 
 					> .channel
@@ -451,7 +455,7 @@ export default Vue.extend({
 						background $theme-color
 						border-radius 4px
 
-				> mk-poll
+				> .mk-poll
 					font-size 80%
 
 				> .repost
@@ -466,7 +470,7 @@ export default Vue.extend({
 						font-size 28px
 						background #fff
 
-					> mk-post-preview
+					> .mk-post-preview
 						padding 16px
 						border dashed 1px #c0dac6
 						border-radius 8px
diff --git a/src/web/app/desktop/views/components/repost-form.vue b/src/web/app/desktop/views/components/repost-form.vue
index 9e9f7174f..f0e4a2bdf 100644
--- a/src/web/app/desktop/views/components/repost-form.vue
+++ b/src/web/app/desktop/views/components/repost-form.vue
@@ -58,7 +58,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 .mk-repost-form
 
-	> mk-post-preview
+	> .mk-post-preview
 		margin 16px 22px
 
 	> div
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index b24e78fe4..63b36ff54 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -104,7 +104,7 @@ export default Vue.extend({
 	border solid 1px rgba(0, 0, 0, 0.075)
 	border-radius 6px
 
-	> mk-following-setuper
+	> .mk-following-setuper
 		border-bottom solid 1px #eee
 
 	> .loading
diff --git a/src/web/app/desktop/views/components/ui-header-notifications.vue b/src/web/app/desktop/views/components/ui-header-notifications.vue
index 779ee4886..5ffa28c91 100644
--- a/src/web/app/desktop/views/components/ui-header-notifications.vue
+++ b/src/web/app/desktop/views/components/ui-header-notifications.vue
@@ -148,7 +148,7 @@ export default Vue.extend({
 			border-bottom solid 14px #fff
 			border-left solid 14px transparent
 
-		> mk-notifications
+		> .mk-notifications
 			max-height 350px
 			font-size 1rem
 			overflow auto
diff --git a/src/web/app/desktop/views/pages/user/user-friends.vue b/src/web/app/desktop/views/pages/user/user-friends.vue
index eed874897..15fb7a96e 100644
--- a/src/web/app/desktop/views/pages/user/user-friends.vue
+++ b/src/web/app/desktop/views/pages/user/user-friends.vue
@@ -109,7 +109,7 @@ export default Vue.extend({
 				line-height 16px
 				color #ccc
 
-		> mk-follow-button
+		> .mk-follow-button
 			position absolute
 			top 16px
 			right 16px
diff --git a/src/web/app/desktop/views/pages/user/user-home.vue b/src/web/app/desktop/views/pages/user/user-home.vue
index 926a1f571..dc0a03dab 100644
--- a/src/web/app/desktop/views/pages/user/user-home.vue
+++ b/src/web/app/desktop/views/pages/user/user-home.vue
@@ -51,7 +51,7 @@ export default Vue.extend({
 		padding 16px
 		width calc(100% - 275px * 2)
 
-		> mk-user-timeline
+		> .mk-user-timeline
 			border solid 1px rgba(0, 0, 0, 0.075)
 			border-radius 6px
 
diff --git a/src/web/app/desktop/views/pages/user/user-profile.vue b/src/web/app/desktop/views/pages/user/user-profile.vue
index 6b88b47ac..66385ab2e 100644
--- a/src/web/app/desktop/views/pages/user/user-profile.vue
+++ b/src/web/app/desktop/views/pages/user/user-profile.vue
@@ -87,7 +87,7 @@ export default Vue.extend({
 		padding 16px
 		border-top solid 1px #eee
 
-		> mk-big-follow-button
+		> .mk-big-follow-button
 			width 100%
 
 		> .followed
diff --git a/src/web/app/mobile/views/components/drive.vue b/src/web/app/mobile/views/components/drive.vue
index a3dd95973..c842caacb 100644
--- a/src/web/app/mobile/views/components/drive.vue
+++ b/src/web/app/mobile/views/components/drive.vue
@@ -509,11 +509,11 @@ export default Vue.extend({
 				color #777
 
 		> .folders
-			> mk-drive-folder
+			> .mk-drive-folder
 				border-bottom solid 1px #eee
 
 		> .files
-			> mk-drive-file
+			> .mk-drive-file
 				border-bottom solid 1px #eee
 
 			> .more
diff --git a/src/web/app/mobile/views/components/friends-maker.vue b/src/web/app/mobile/views/components/friends-maker.vue
index a7a81aeb7..45ee4a644 100644
--- a/src/web/app/mobile/views/components/friends-maker.vue
+++ b/src/web/app/mobile/views/components/friends-maker.vue
@@ -72,7 +72,7 @@ export default Vue.extend({
 		padding 16px
 		background #eee
 
-		> mk-user-card
+		> .mk-user-card
 			&:not(:last-child)
 				margin-right 16px
 
diff --git a/src/web/app/mobile/views/components/notification.vue b/src/web/app/mobile/views/components/notification.vue
index 1b4608724..98390f1c1 100644
--- a/src/web/app/mobile/views/components/notification.vue
+++ b/src/web/app/mobile/views/components/notification.vue
@@ -120,7 +120,7 @@ export default Vue.extend({
 	padding 16px
 	overflow-wrap break-word
 
-	> mk-time
+	> .mk-time
 		display inline
 		position absolute
 		top 16px
diff --git a/src/web/app/mobile/views/components/notifications.vue b/src/web/app/mobile/views/components/notifications.vue
index 3cad1d514..8813bef5b 100644
--- a/src/web/app/mobile/views/components/notifications.vue
+++ b/src/web/app/mobile/views/components/notifications.vue
@@ -114,7 +114,7 @@ export default Vue.extend({
 
 	> .notifications
 
-		> mk-notification
+		> .mk-notification
 			margin 0 auto
 			max-width 500px
 			border-bottom solid 1px rgba(0, 0, 0, 0.05)
diff --git a/src/web/app/mobile/views/components/post-card.vue b/src/web/app/mobile/views/components/post-card.vue
index 4dd6ceb28..08a2bebfc 100644
--- a/src/web/app/mobile/views/components/post-card.vue
+++ b/src/web/app/mobile/views/components/post-card.vue
@@ -77,7 +77,7 @@ export default Vue.extend({
 				height 20px
 				background linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #fff 100%)
 
-		> mk-time
+		> .mk-time
 			display inline-block
 			padding 8px
 			color #aaa
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index ba28e7be3..da4f3fee7 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -285,7 +285,7 @@ export default Vue.extend({
 				@media (min-width 500px)
 					font-size 24px
 
-				> mk-url-preview
+				> .mk-url-preview
 					margin-top 8px
 
 			> .media
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index 49f6a94d8..091056bcd 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -130,7 +130,7 @@ export default Vue.extend({
 		max-width 500px
 		margin 0 auto
 
-		> mk-post-preview
+		> .mk-post-preview
 			padding 16px
 
 		> .attaches
@@ -159,7 +159,7 @@ export default Vue.extend({
 						background-size cover
 						background-position center center
 
-		> mk-uploader
+		> .mk-uploader
 			margin 8px 0 0 0
 			padding 8px
 
diff --git a/src/web/app/mobile/views/components/posts-post.vue b/src/web/app/mobile/views/components/posts-post.vue
index 4dd82e648..56b42d9c2 100644
--- a/src/web/app/mobile/views/components/posts-post.vue
+++ b/src/web/app/mobile/views/components/posts-post.vue
@@ -201,7 +201,7 @@ export default Vue.extend({
 			.name
 				font-weight bold
 
-		> mk-time
+		> .mk-time
 			position absolute
 			top 8px
 			right 16px
@@ -217,7 +217,7 @@ export default Vue.extend({
 	> .reply-to
 		background rgba(0, 0, 0, 0.0125)
 
-		> mk-post-preview
+		> .mk-post-preview
 			background transparent
 
 	> article
@@ -359,7 +359,7 @@ export default Vue.extend({
 					font-size 12px
 					color #ccc
 
-				> mk-poll
+				> .mk-poll
 					font-size 80%
 
 				> .repost
@@ -374,7 +374,7 @@ export default Vue.extend({
 						font-size 28px
 						background #fff
 
-					> mk-post-preview
+					> .mk-post-preview
 						padding 16px
 						border dashed 1px #c0dac6
 						border-radius 8px
diff --git a/src/web/app/mobile/views/components/user-card.vue b/src/web/app/mobile/views/components/user-card.vue
index f70def48f..729421616 100644
--- a/src/web/app/mobile/views/components/user-card.vue
+++ b/src/web/app/mobile/views/components/user-card.vue
@@ -55,7 +55,7 @@ export default Vue.extend({
 		font-size 15px
 		color #ccc
 
-	> mk-follow-button
+	> .mk-follow-button
 		display inline-block
 		margin 8px 0 16px 0
 
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index 4cc152c1e..6c784b05f 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -141,7 +141,7 @@ export default Vue.extend({
 							border 4px solid #313a42
 							border-radius 12px
 
-				> mk-follow-button
+				> .mk-follow-button
 					float right
 					height 40px
 
@@ -199,7 +199,7 @@ export default Vue.extend({
 					> i
 						font-size 14px
 
-			> mk-activity-table
+			> .mk-activity-table
 				margin 12px 0 0 0
 
 		> nav
diff --git a/src/web/app/mobile/views/pages/user/home-friends.vue b/src/web/app/mobile/views/pages/user/home-friends.vue
index 2a7e8b961..7c5a50559 100644
--- a/src/web/app/mobile/views/pages/user/home-friends.vue
+++ b/src/web/app/mobile/views/pages/user/home-friends.vue
@@ -37,7 +37,7 @@ export default Vue.extend({
 		white-space nowrap
 		padding 8px
 
-		> mk-user-card
+		> .mk-user-card
 			&:not(:last-child)
 				margin-right 8px
 
diff --git a/src/web/app/mobile/views/pages/user/home.vue b/src/web/app/mobile/views/pages/user/home.vue
index 56b928559..a23825f22 100644
--- a/src/web/app/mobile/views/pages/user/home.vue
+++ b/src/web/app/mobile/views/pages/user/home.vue
@@ -59,7 +59,7 @@ export default Vue.extend({
 	max-width 600px
 	margin 0 auto
 
-	> mk-post-detail
+	> .mk-post-detail
 		margin 0 0 8px 0
 
 	> section
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 2b66dd7f7..9a85e9189 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -119,7 +119,6 @@ module.exports = Object.keys(langs).map(lang => {
 		resolveLoader: {
 			modules: ['node_modules', './webpack/loaders']
 		},
-		cache: true,
-		devtool: 'eval'
+		cache: true
 	};
 });

From 8e9e796b438649c4e6dc4b2427e8b2a73e1922ae Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 03:18:48 +0900
Subject: [PATCH 171/286] wip

---
 src/web/app/common/views/components/index.ts  |   2 +
 src/web/app/common/views/components/time.vue  |   2 +-
 .../desktop/views/components/post-preview.vue | 120 +++++++++---------
 .../views/components/repost-form-window.vue   |  10 +-
 .../desktop/views/components/repost-form.vue  |   9 +-
 .../desktop/views/directives/user-preview.ts  |   1 -
 6 files changed, 71 insertions(+), 73 deletions(-)

diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index e3f105f58..740b73f9f 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -12,6 +12,7 @@ import images from './images.vue';
 import uploader from './uploader.vue';
 import specialMessage from './special-message.vue';
 import streamIndicator from './stream-indicator.vue';
+import ellipsis from './ellipsis.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
@@ -25,3 +26,4 @@ Vue.component('mk-images', images);
 Vue.component('mk-uploader', uploader);
 Vue.component('mk-special-message', specialMessage);
 Vue.component('mk-stream-indicator', streamIndicator);
+Vue.component('mk-ellipsis', ellipsis);
diff --git a/src/web/app/common/views/components/time.vue b/src/web/app/common/views/components/time.vue
index 3c856d3f2..6e0d2b0dc 100644
--- a/src/web/app/common/views/components/time.vue
+++ b/src/web/app/common/views/components/time.vue
@@ -1,5 +1,5 @@
 <template>
-<time>
+<time class="mk-time">
 	<span v-if=" mode == 'relative' ">{{ relative }}</span>
 	<span v-if=" mode == 'absolute' ">{{ absolute }}</span>
 	<span v-if=" mode == 'detail' ">{{ absolute }} ({{ relative }})</span>
diff --git a/src/web/app/desktop/views/components/post-preview.vue b/src/web/app/desktop/views/components/post-preview.vue
index f22b28153..7452bffe2 100644
--- a/src/web/app/desktop/views/components/post-preview.vue
+++ b/src/web/app/desktop/views/components/post-preview.vue
@@ -6,7 +6,7 @@
 	<div class="main">
 		<header>
 			<a class="name" :href="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</a>
-			<span class="username">@{ post.user.username }</span>
+			<span class="username">@{{ post.user.username }}</span>
 			<a class="time" :href="`/${post.user.username}/${post.id}`">
 			<mk-time :time="post.created_at"/></a>
 		</header>
@@ -31,78 +31,72 @@ export default Vue.extend({
 });
 </script>
 
-
 <style lang="stylus" scoped>
 .mk-post-preview
-	display block
-	margin 0
-	padding 0
 	font-size 0.9em
 	background #fff
 
-	> article
+	&:after
+		content ""
+		display block
+		clear both
 
-		&:after
-			content ""
+	&:hover
+		> .main > footer > button
+			color #888
+
+	> .avatar-anchor
+		display block
+		float left
+		margin 0 16px 0 0
+
+		> .avatar
 			display block
-			clear both
+			width 52px
+			height 52px
+			margin 0
+			border-radius 8px
+			vertical-align bottom
 
-		&:hover
-			> .main > footer > button
-				color #888
+	> .main
+		float left
+		width calc(100% - 68px)
 
-		> .avatar-anchor
-			display block
-			float left
-			margin 0 16px 0 0
+		> header
+			display flex
+			margin 4px 0
+			white-space nowrap
 
-			> .avatar
-				display block
-				width 52px
-				height 52px
+			> .name
+				margin 0 .5em 0 0
+				padding 0
+				color #607073
+				font-size 1em
+				line-height 1.1em
+				font-weight 700
+				text-align left
+				text-decoration none
+				white-space normal
+
+				&:hover
+					text-decoration underline
+
+			> .username
+				text-align left
+				margin 0 .5em 0 0
+				color #d1d8da
+
+			> .time
+				margin-left auto
+				color #b2b8bb
+
+		> .body
+
+			> .text
+				cursor default
 				margin 0
-				border-radius 8px
-				vertical-align bottom
-
-		> .main
-			float left
-			width calc(100% - 68px)
-
-			> header
-				display flex
-				margin 4px 0
-				white-space nowrap
-
-				> .name
-					margin 0 .5em 0 0
-					padding 0
-					color #607073
-					font-size 1em
-					line-height 1.1em
-					font-weight 700
-					text-align left
-					text-decoration none
-					white-space normal
-
-					&:hover
-						text-decoration underline
-
-				> .username
-					text-align left
-					margin 0 .5em 0 0
-					color #d1d8da
-
-				> .time
-					margin-left auto
-					color #b2b8bb
-
-			> .body
-
-				> .text
-					cursor default
-					margin 0
-					padding 0
-					font-size 1.1em
-					color #717171
+				padding 0
+				font-size 1.1em
+				color #717171
 
 </style>
diff --git a/src/web/app/desktop/views/components/repost-form-window.vue b/src/web/app/desktop/views/components/repost-form-window.vue
index 6f06faaba..7db5adbff 100644
--- a/src/web/app/desktop/views/components/repost-form-window.vue
+++ b/src/web/app/desktop/views/components/repost-form-window.vue
@@ -1,9 +1,7 @@
 <template>
 <mk-window ref="window" is-modal @closed="$destroy">
 	<span slot="header" :class="$style.header">%fa:retweet%%i18n:desktop.tags.mk-repost-form-window.title%</span>
-	<div slot="content">
-		<mk-repost-form ref="form" :post="post" @posted="$refs.window.close" @canceled="$refs.window.close"/>
-	</div>
+	<mk-repost-form ref="form" :post="post" @posted="onPosted" @canceled="onCanceled"/>
 </mk-window>
 </template>
 
@@ -25,6 +23,12 @@ export default Vue.extend({
 					(this.$refs.window as any).close();
 				}
 			}
+		},
+		onPosted() {
+			(this.$refs.window as any).close();
+		},
+		onCanceled() {
+			(this.$refs.window as any).close();
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/repost-form.vue b/src/web/app/desktop/views/components/repost-form.vue
index f0e4a2bdf..04b045ad4 100644
--- a/src/web/app/desktop/views/components/repost-form.vue
+++ b/src/web/app/desktop/views/components/repost-form.vue
@@ -3,7 +3,7 @@
 	<mk-post-preview :post="post"/>
 	<template v-if="!quote">
 		<footer>
-			<a class="quote" v-if="!quote" @click="onquote">%i18n:desktop.tags.mk-repost-form.quote%</a>
+			<a class="quote" v-if="!quote" @click="onQuote">%i18n:desktop.tags.mk-repost-form.quote%</a>
 			<button class="cancel" @click="cancel">%i18n:desktop.tags.mk-repost-form.cancel%</button>
 			<button class="ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:desktop.tags.mk-repost-form.reposting%' : '%i18n:desktop.tags.mk-repost-form.repost%' }}</button>
 		</footer>
@@ -46,7 +46,9 @@ export default Vue.extend({
 		onQuote() {
 			this.quote = true;
 
-			(this.$refs.form as any).focus();
+			this.$nextTick(() => {
+				(this.$refs.form as any).focus();
+			});
 		},
 		onChildFormPosted() {
 			this.$emit('posted');
@@ -61,9 +63,6 @@ export default Vue.extend({
 	> .mk-post-preview
 		margin 16px 22px
 
-	> div
-		padding 16px
-
 	> footer
 		height 72px
 		background lighten($theme-color, 95%)
diff --git a/src/web/app/desktop/views/directives/user-preview.ts b/src/web/app/desktop/views/directives/user-preview.ts
index 7d6993667..322302bcf 100644
--- a/src/web/app/desktop/views/directives/user-preview.ts
+++ b/src/web/app/desktop/views/directives/user-preview.ts
@@ -55,7 +55,6 @@ export default {
 	},
 	unbind(el, binding, vn) {
 		const self = vn.context._userPreviewDirective_;
-		console.log('unbound:', self.user);
 		clearTimeout(self.showTimer);
 		clearTimeout(self.hideTimer);
 		self.close();

From 79705d20354775ae515f4fa47242837b089d18b8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 03:53:21 +0900
Subject: [PATCH 172/286] wip

---
 src/web/app/init.ts | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 4ef2a8921..0cea587a1 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -20,6 +20,12 @@ require('./common/views/directives');
 // Register global components
 require('./common/views/components');
 
+Vue.mixin({
+	destroyed(this: any) {
+		this.$el.parentNode.removeChild(this.$el);
+	}
+});
+
 import App from './app.vue';
 
 import checkForUpdate from './common/scripts/check-for-update';

From c81c4c224aa2ebe44f69875b80a272152a2401d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 09:19:16 +0900
Subject: [PATCH 173/286] wip

---
 src/web/app/mobile/tags/page/search.tag   | 26 ---------
 src/web/app/mobile/tags/search-posts.tag  | 42 --------------
 src/web/app/mobile/tags/search.tag        | 16 ------
 src/web/app/mobile/views/pages/search.vue | 70 +++++++++++++++++++++++
 4 files changed, 70 insertions(+), 84 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/page/search.tag
 delete mode 100644 src/web/app/mobile/tags/search-posts.tag
 delete mode 100644 src/web/app/mobile/tags/search.tag
 create mode 100644 src/web/app/mobile/views/pages/search.vue

diff --git a/src/web/app/mobile/tags/page/search.tag b/src/web/app/mobile/tags/page/search.tag
deleted file mode 100644
index 44af3a2ad..000000000
--- a/src/web/app/mobile/tags/page/search.tag
+++ /dev/null
@@ -1,26 +0,0 @@
-<mk-search-page>
-	<mk-ui ref="ui">
-		<mk-search ref="search" query={ parent.opts.query }/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import ui from '../../scripts/ui-event';
-		import Progress from '../../../common/scripts/loading';
-
-		this.on('mount', () => {
-			document.title = `%i18n:mobile.tags.mk-search-page.search%: ${this.opts.query} | Misskey`
-			// TODO: クエリをHTMLエスケープ
-			ui.trigger('title', '%fa:search%' + this.opts.query);
-			document.documentElement.style.background = '#313a42';
-
-			Progress.start();
-
-			this.$refs.ui.refs.search.on('loaded', () => {
-				Progress.done();
-			});
-		});
-	</script>
-</mk-search-page>
diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag
deleted file mode 100644
index 7b4d73f2d..000000000
--- a/src/web/app/mobile/tags/search-posts.tag
+++ /dev/null
@@ -1,42 +0,0 @@
-<mk-search-posts>
-	<mk-timeline init={ init } more={ more } empty={ '%i18n:mobile.tags.mk-search-posts.empty%'.replace('{}', query) }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 8px auto
-			max-width 500px
-			width calc(100% - 16px)
-			background #fff
-			border-radius 8px
-			box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
-
-			@media (min-width 500px)
-				margin 16px auto
-				width calc(100% - 32px)
-	</style>
-	<script lang="typescript">
-		import parse from '../../common/scripts/parse-search-query';
-
-		this.mixin('api');
-
-		this.limit = 30;
-		this.offset = 0;
-
-		this.query = this.opts.query;
-
-		this.init = new Promise((res, rej) => {
-			this.$root.$data.os.api('posts/search', parse(this.query)).then(posts => {
-				res(posts);
-				this.$emit('loaded');
-			});
-		});
-
-		this.more = () => {
-			this.offset += this.limit;
-			return this.$root.$data.os.api('posts/search', Object.assign({}, parse(this.query), {
-				limit: this.limit,
-				offset: this.offset
-			}));
-		};
-	</script>
-</mk-search-posts>
diff --git a/src/web/app/mobile/tags/search.tag b/src/web/app/mobile/tags/search.tag
deleted file mode 100644
index 61f3093e0..000000000
--- a/src/web/app/mobile/tags/search.tag
+++ /dev/null
@@ -1,16 +0,0 @@
-<mk-search>
-	<mk-search-posts ref="posts" query={ query }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		this.query = this.opts.query;
-
-		this.on('mount', () => {
-			this.$refs.posts.on('loaded', () => {
-				this.$emit('loaded');
-			});
-		});
-	</script>
-</mk-search>
diff --git a/src/web/app/mobile/views/pages/search.vue b/src/web/app/mobile/views/pages/search.vue
new file mode 100644
index 000000000..89710d7c2
--- /dev/null
+++ b/src/web/app/mobile/views/pages/search.vue
@@ -0,0 +1,70 @@
+<template>
+<mk-ui>
+	<span slot="header">%fa:search% {{ query }}</span>
+	<main v-if="!fetching">
+		<mk-posts :class="$style.posts">
+			<span v-if="posts.length == 0">{{ '%i18n:mobile.tags.mk-search-posts.empty%'.replace('{}', query) }}</span>
+			<button v-if="canFetchMore" @click="more" :disabled="fetching" slot="tail">
+				<span v-if="!fetching">%i18n:mobile.tags.mk-timeline.load-more%</span>
+				<span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span>
+			</button>
+		</mk-posts>
+	</main>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+import parse from '../../../common/scripts/parse-search-query';
+
+const limit = 30;
+
+export default Vue.extend({
+	props: ['query'],
+	data() {
+		return {
+			fetching: true,
+			posts: [],
+			offset: 0
+		};
+	},
+	mounted() {
+		document.title = `%i18n:mobile.tags.mk-search-page.search%: ${this.query} | Misskey`;
+		document.documentElement.style.background = '#313a42';
+
+		Progress.start();
+
+		this.$root.$data.os.api('posts/search', Object.assign({}, parse(this.query), {
+			limit: limit
+		})).then(posts => {
+			this.posts = posts;
+			this.fetching = false;
+			Progress.done();
+		});
+	},
+	methods: {
+		more() {
+			this.offset += limit;
+			return this.$root.$data.os.api('posts/search', Object.assign({}, parse(this.query), {
+				limit: limit,
+				offset: this.offset
+			}));
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.posts
+	margin 8px auto
+	max-width 500px
+	width calc(100% - 16px)
+	background #fff
+	border-radius 8px
+	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+
+	@media (min-width 500px)
+		margin 16px auto
+		width calc(100% - 32px)
+</style>

From 283c64e6f1a499fa73c63ecd3017d16160590e9d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 09:19:37 +0900
Subject: [PATCH 174/286] wip

---
 src/web/app/mobile/views/pages/search.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/mobile/views/pages/search.vue b/src/web/app/mobile/views/pages/search.vue
index 89710d7c2..02cdb1600 100644
--- a/src/web/app/mobile/views/pages/search.vue
+++ b/src/web/app/mobile/views/pages/search.vue
@@ -2,7 +2,7 @@
 <mk-ui>
 	<span slot="header">%fa:search% {{ query }}</span>
 	<main v-if="!fetching">
-		<mk-posts :class="$style.posts">
+		<mk-posts :class="$style.posts" :posts="posts">
 			<span v-if="posts.length == 0">{{ '%i18n:mobile.tags.mk-search-posts.empty%'.replace('{}', query) }}</span>
 			<button v-if="canFetchMore" @click="more" :disabled="fetching" slot="tail">
 				<span v-if="!fetching">%i18n:mobile.tags.mk-timeline.load-more%</span>

From 61b95e0c26d086134366b0377354df581e360fc1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 17 Feb 2018 18:14:23 +0900
Subject: [PATCH 175/286] wip

---
 src/web/app/common/define-widget.ts           |   2 +-
 src/web/app/common/views/components/index.ts  |  12 ++
 .../views/components/widgets/photo-stream.vue |   2 +-
 .../views/components/widgets/slideshow.vue    |   2 +-
 .../-tags/select-folder-from-drive-window.tag | 112 ------------------
 .../app/desktop/api/choose-drive-folder.ts    |  17 +++
 src/web/app/desktop/script.ts                 |   6 +-
 .../choose-file-from-drive-window.vue         |   5 +-
 .../choose-folder-from-drive-window.vue       | 112 ++++++++++++++++++
 .../views/components/widgets/messaging.vue    |   0
 src/web/app/init.ts                           |  30 ++++-
 11 files changed, 179 insertions(+), 121 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/select-folder-from-drive-window.tag
 create mode 100644 src/web/app/desktop/api/choose-drive-folder.ts
 create mode 100644 src/web/app/desktop/views/components/choose-folder-from-drive-window.vue
 rename src/web/app/{common => desktop}/views/components/widgets/messaging.vue (100%)

diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index 782a69a62..4e83e37c6 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -26,7 +26,7 @@ export default function<T extends object>(data: {
 		},
 		data() {
 			return {
-				props: data.props || {}
+				props: data.props || {} as T
 			};
 		},
 		watch: {
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 740b73f9f..209a68fe5 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -13,6 +13,12 @@ import uploader from './uploader.vue';
 import specialMessage from './special-message.vue';
 import streamIndicator from './stream-indicator.vue';
 import ellipsis from './ellipsis.vue';
+import wNav from './widgets/nav.vue';
+import wCalendar from './widgets/calendar.vue';
+import wPhotoStream from './widgets/photo-stream.vue';
+import wSlideshow from './widgets/slideshow.vue';
+import wTips from './widgets/tips.vue';
+import wDonation from './widgets/donation.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
@@ -27,3 +33,9 @@ Vue.component('mk-uploader', uploader);
 Vue.component('mk-special-message', specialMessage);
 Vue.component('mk-stream-indicator', streamIndicator);
 Vue.component('mk-ellipsis', ellipsis);
+Vue.component('mkw-nav', wNav);
+Vue.component('mkw-calendar', wCalendar);
+Vue.component('mkw-photo-stream', wPhotoStream);
+Vue.component('mkw-slideshoe', wSlideshow);
+Vue.component('mkw-tips', wTips);
+Vue.component('mkw-donation', wDonation);
diff --git a/src/web/app/common/views/components/widgets/photo-stream.vue b/src/web/app/common/views/components/widgets/photo-stream.vue
index 12e568ca0..afbdc2162 100644
--- a/src/web/app/common/views/components/widgets/photo-stream.vue
+++ b/src/web/app/common/views/components/widgets/photo-stream.vue
@@ -44,7 +44,7 @@ export default define({
 		this.$root.$data.os.stream.dispose(this.connectionId);
 	},
 	methods: {
-		onStreamDriveFileCreated(file) {
+		onDriveFileCreated(file) {
 			if (/^image\/.+$/.test(file.type)) {
 				this.images.unshift(file);
 				if (this.images.length > 9) this.images.pop();
diff --git a/src/web/app/common/views/components/widgets/slideshow.vue b/src/web/app/common/views/components/widgets/slideshow.vue
index 6dcd453e2..c24e3003c 100644
--- a/src/web/app/common/views/components/widgets/slideshow.vue
+++ b/src/web/app/common/views/components/widgets/slideshow.vue
@@ -102,7 +102,7 @@ export default define({
 			});
 		},
 		choose() {
-			this.wapi_selectDriveFolder().then(folder => {
+			this.$root.$data.api.chooseDriveFolder().then(folder => {
 				this.props.folder = folder ? folder.id : null;
 				this.fetch();
 			});
diff --git a/src/web/app/desktop/-tags/select-folder-from-drive-window.tag b/src/web/app/desktop/-tags/select-folder-from-drive-window.tag
deleted file mode 100644
index 2f98f30a6..000000000
--- a/src/web/app/desktop/-tags/select-folder-from-drive-window.tag
+++ /dev/null
@@ -1,112 +0,0 @@
-<mk-select-folder-from-drive-window>
-	<mk-window ref="window" is-modal={ true } width={ '800px' } height={ '500px' }>
-		<yield to="header">
-			<mk-raw content={ parent.title }/>
-		</yield>
-		<yield to="content">
-			<mk-drive-browser ref="browser"/>
-			<div>
-				<button class="cancel" @click="parent.close">キャンセル</button>
-				<button class="ok" @click="parent.ok">決定</button>
-			</div>
-		</yield>
-	</mk-window>
-	<style lang="stylus" scoped>
-		:scope
-			> mk-window
-				[data-yield='header']
-					> mk-raw
-						> [data-fa]
-							margin-right 4px
-
-				[data-yield='content']
-					> mk-drive-browser
-						height calc(100% - 72px)
-
-					> div
-						height 72px
-						background lighten($theme-color, 95%)
-
-						.ok
-						.cancel
-							display block
-							position absolute
-							bottom 16px
-							cursor pointer
-							padding 0
-							margin 0
-							width 120px
-							height 40px
-							font-size 1em
-							outline none
-							border-radius 4px
-
-							&:focus
-								&:after
-									content ""
-									pointer-events none
-									position absolute
-									top -5px
-									right -5px
-									bottom -5px
-									left -5px
-									border 2px solid rgba($theme-color, 0.3)
-									border-radius 8px
-
-							&:disabled
-								opacity 0.7
-								cursor default
-
-						.ok
-							right 16px
-							color $theme-color-foreground
-							background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
-							border solid 1px lighten($theme-color, 15%)
-
-							&:not(:disabled)
-								font-weight bold
-
-							&:hover:not(:disabled)
-								background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
-								border-color $theme-color
-
-							&:active:not(:disabled)
-								background $theme-color
-								border-color $theme-color
-
-						.cancel
-							right 148px
-							color #888
-							background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
-							border solid 1px #e2e2e2
-
-							&:hover
-								background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
-								border-color #dcdcdc
-
-							&:active
-								background #ececec
-								border-color #dcdcdc
-
-	</style>
-	<script lang="typescript">
-		this.files = [];
-
-		this.title = this.opts.title || '%fa:R folder%フォルダを選択';
-
-		this.on('mount', () => {
-			this.$refs.window.on('closed', () => {
-				this.$destroy();
-			});
-		});
-
-		this.close = () => {
-			this.$refs.window.close();
-		};
-
-		this.ok = () => {
-			this.$emit('selected', this.$refs.window.refs.browser.folder);
-			this.$refs.window.close();
-		};
-	</script>
-</mk-select-folder-from-drive-window>
diff --git a/src/web/app/desktop/api/choose-drive-folder.ts b/src/web/app/desktop/api/choose-drive-folder.ts
new file mode 100644
index 000000000..a5116f7bc
--- /dev/null
+++ b/src/web/app/desktop/api/choose-drive-folder.ts
@@ -0,0 +1,17 @@
+import MkChooseFolderFromDriveWindow from '../../../common/views/components/choose-folder-from-drive-window.vue';
+
+export default function(this: any, opts) {
+	return new Promise((res, rej) => {
+		const o = opts || {};
+		const w = new MkChooseFolderFromDriveWindow({
+			parent: this,
+			propsData: {
+				title: o.title
+			}
+		}).$mount();
+		w.$once('selected', folder => {
+			res(folder);
+		});
+		document.body.appendChild(w.$el);
+	});
+}
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index 1377965ea..cd894170e 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -10,6 +10,8 @@ import fuckAdBlock from './scripts/fuck-ad-block';
 import HomeStreamManager from '../common/scripts/streaming/home-stream-manager';
 import composeNotification from '../common/scripts/compose-notification';
 
+import chooseDriveFolder from './api/choose-drive-folder';
+
 import MkIndex from './views/pages/index.vue';
 
 /**
@@ -27,7 +29,9 @@ init(async (launch) => {
 	// Register components
 	require('./views/components');
 
-	const app = launch();
+	const app = launch({
+		chooseDriveFolder
+	});
 
 	/**
 	 * Init Notification
diff --git a/src/web/app/desktop/views/components/choose-file-from-drive-window.vue b/src/web/app/desktop/views/components/choose-file-from-drive-window.vue
index ed9ca6466..5aa226f4c 100644
--- a/src/web/app/desktop/views/components/choose-file-from-drive-window.vue
+++ b/src/web/app/desktop/views/components/choose-file-from-drive-window.vue
@@ -14,7 +14,7 @@
 	/>
 	<div :class="$style.footer">
 		<button :class="$style.upload" title="PCからドライブにファイルをアップロード" @click="upload">%fa:upload%</button>
-		<button :class="$style.cancel" @click="close">キャンセル</button>
+		<button :class="$style.cancel" @click="cancel">キャンセル</button>
 		<button :class="$style.ok" :disabled="multiple && files.length == 0" @click="ok">決定</button>
 	</div>
 </mk-window>
@@ -50,6 +50,9 @@ export default Vue.extend({
 		ok() {
 			this.$emit('selected', this.multiple ? this.files : this.files[0]);
 			(this.$refs.window as any).close();
+		},
+		cancel() {
+			(this.$refs.window as any).close();
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/web/app/desktop/views/components/choose-folder-from-drive-window.vue
new file mode 100644
index 000000000..0e598937e
--- /dev/null
+++ b/src/web/app/desktop/views/components/choose-folder-from-drive-window.vue
@@ -0,0 +1,112 @@
+<template>
+<mk-window ref="window" is-modal width='800px' height='500px' @closed="$destroy">
+	<span slot="header">
+		<span v-html="title" :class="$style.title"></span>
+	</span>
+
+	<mk-drive
+		ref="browser"
+		:class="$style.browser"
+		:multiple="false"
+	/>
+	<div :class="$style.footer">
+		<button :class="$style.cancel" @click="close">キャンセル</button>
+		<button :class="$style.ok" @click="ok">決定</button>
+	</div>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		title: {
+			default: '%fa:R folder%フォルダを選択'
+		}
+	},
+	methods: {
+		ok() {
+			this.$emit('selected', (this.$refs.browser as any).folder);
+			(this.$refs.window as any).close();
+		},
+		cancel() {
+			(this.$refs.window as any).close();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.title
+	> [data-fa]
+		margin-right 4px
+
+.browser
+	height calc(100% - 72px)
+
+.footer
+	height 72px
+	background lighten($theme-color, 95%)
+
+.ok
+.cancel
+	display block
+	position absolute
+	bottom 16px
+	cursor pointer
+	padding 0
+	margin 0
+	width 120px
+	height 40px
+	font-size 1em
+	outline none
+	border-radius 4px
+
+	&:focus
+		&:after
+			content ""
+			pointer-events none
+			position absolute
+			top -5px
+			right -5px
+			bottom -5px
+			left -5px
+			border 2px solid rgba($theme-color, 0.3)
+			border-radius 8px
+
+	&:disabled
+		opacity 0.7
+		cursor default
+
+.ok
+	right 16px
+	color $theme-color-foreground
+	background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+	border solid 1px lighten($theme-color, 15%)
+
+	&:not(:disabled)
+		font-weight bold
+
+	&:hover:not(:disabled)
+		background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
+		border-color $theme-color
+
+	&:active:not(:disabled)
+		background $theme-color
+		border-color $theme-color
+
+.cancel
+	right 148px
+	color #888
+	background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
+	border solid 1px #e2e2e2
+
+	&:hover
+		background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
+		border-color #dcdcdc
+
+	&:active
+		background #ececec
+		border-color #dcdcdc
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/messaging.vue b/src/web/app/desktop/views/components/widgets/messaging.vue
similarity index 100%
rename from src/web/app/common/views/components/widgets/messaging.vue
rename to src/web/app/desktop/views/components/widgets/messaging.vue
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 0cea587a1..450327a58 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -22,7 +22,9 @@ require('./common/views/components');
 
 Vue.mixin({
 	destroyed(this: any) {
-		this.$el.parentNode.removeChild(this.$el);
+		if (this.$el.parentNode) {
+			this.$el.parentNode.removeChild(this.$el);
+		}
 	}
 });
 
@@ -74,18 +76,38 @@ if (localStorage.getItem('should-refresh') == 'true') {
 	location.reload(true);
 }
 
+type API = {
+	chooseDriveFile: (opts: {
+		title: string;
+		currentFolder: any;
+		multiple: boolean;
+	}) => Promise<any>;
+
+	chooseDriveFolder: (opts: {
+		title: string;
+		currentFolder: any;
+	}) => Promise<any>;
+};
+
 // MiOSを初期化してコールバックする
-export default (callback: (launch: () => Vue) => void, sw = false) => {
+export default (callback: (launch: (api: API) => Vue) => void, sw = false) => {
 	const mios = new MiOS(sw);
 
+	Vue.mixin({
+		data: {
+			$os: mios
+		}
+	});
+
 	mios.init(() => {
 		// アプリ基底要素マウント
 		document.body.innerHTML = '<div id="app"></div>';
 
-		const launch = () => {
+		const launch = (api: API) => {
 			return new Vue({
 				data: {
-					os: mios
+					os: mios,
+					api: api
 				},
 				router: new VueRouter({
 					mode: 'history'

From 99b34993640eb91a591faa4bccf7d7b6f176ad97 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 12:35:18 +0900
Subject: [PATCH 176/286] wip

---
 .../views/components/messaging-form.vue       |   2 +-
 .../views/components/messaging-message.vue    |   4 +-
 .../views/components/messaging-room.vue       |  10 +-
 .../app/common/views/components/messaging.vue |  12 +-
 src/web/app/common/views/components/poll.vue  |   2 +-
 .../app/common/views/components/post-menu.vue |   2 +-
 .../views/components/reaction-picker.vue      |   2 +-
 .../app/common/views/components/signin.vue    |   4 +-
 .../app/common/views/components/signup.vue    |   6 +-
 .../views/components/stream-indicator.vue     |  12 +-
 .../app/common/views/components/uploader.vue  |   2 +-
 .../views/components/widgets/photo-stream.vue |   8 +-
 .../views/components/widgets/slideshow.vue    |   4 +-
 .../desktop/-tags/drive/browser-window.tag    |  60 --------
 .../desktop/-tags/drive/file-contextmenu.tag  |  99 ------------
 .../-tags/drive/folder-contextmenu.tag        |  63 --------
 src/web/app/desktop/api/choose-drive-file.ts  |  18 +++
 .../app/desktop/api/choose-drive-folder.ts    |   8 +-
 src/web/app/desktop/api/contextmenu.ts        |  16 ++
 src/web/app/desktop/api/dialog.ts             |  19 +++
 src/web/app/desktop/api/input.ts              |  19 +++
 src/web/app/desktop/script.ts                 |   8 +-
 .../desktop/views/components/2fa-setting.vue  |  10 +-
 .../desktop/views/components/api-setting.vue  |   4 +-
 .../views/components/context-menu-menu.vue    | 113 ++++++++++++++
 .../desktop/views/components/context-menu.vue |  74 +++++++++
 .../desktop/views/components/contextmenu.vue  | 142 -----------------
 .../app/desktop/views/components/dialog.vue   |  15 +-
 .../views/components/drive-contextmenu.vue    |  46 ------
 .../desktop/views/components/drive-file.vue   | 123 ++++++++++++---
 .../desktop/views/components/drive-folder.vue |  93 ++++++++---
 .../views/components/drive-nav-folder.vue     |   4 +-
 .../desktop/views/components/drive-window.vue |  53 +++++++
 .../app/desktop/views/components/drive.vue    | 145 ++++++++++++------
 .../views/components/follow-button.vue        |  10 +-
 .../views/components/friends-maker.vue        |   2 +-
 src/web/app/desktop/views/components/home.vue |  26 ++--
 src/web/app/desktop/views/components/index.ts |   8 +
 .../desktop/views/components/input-dialog.vue |  10 +-
 .../views/components/messaging-window.vue     |   1 -
 .../desktop/views/components/mute-setting.vue |   2 +-
 .../views/components/notifications.vue        |  10 +-
 .../views/components/password-setting.vue     |   2 +-
 .../views/components/post-detail-sub.vue      |   2 +-
 .../desktop/views/components/post-detail.vue  |   6 +-
 .../desktop/views/components/post-form.vue    |  18 +--
 .../desktop/views/components/posts-post.vue   |  22 +--
 .../views/components/profile-setting.vue      |  14 +-
 .../desktop/views/components/repost-form.vue  |   2 +-
 .../views/components/sub-post-content.vue     |   2 +-
 .../app/desktop/views/components/timeline.vue |  12 +-
 .../views/components/ui-header-account.vue    |   6 +-
 .../views/components/ui-header-nav.vue        |  14 +-
 .../components/ui-header-notifications.vue    |  12 +-
 .../desktop/views/components/ui-header.vue    |   6 +-
 src/web/app/desktop/views/components/ui.vue   |   2 +-
 .../views/components/user-followers.vue       |   2 +-
 .../views/components/user-following.vue       |   2 +-
 .../desktop/views/components/user-preview.vue |   4 +-
 .../views/components/user-timeline.vue        |   4 +-
 .../desktop/views/components/users-list.vue   |   2 +-
 .../app/desktop/views/components/window.vue   |   6 +-
 src/web/app/desktop/views/pages/home.vue      |   8 +-
 src/web/app/desktop/views/pages/index.vue     |   2 +-
 .../desktop/views/pages/messaging-room.vue    |   2 +-
 src/web/app/desktop/views/pages/post.vue      |   2 +-
 src/web/app/desktop/views/pages/search.vue    |   4 +-
 .../pages/user/user-followers-you-know.vue    |   2 +-
 .../desktop/views/pages/user/user-friends.vue |   2 +-
 .../desktop/views/pages/user/user-header.vue  |   4 +-
 .../desktop/views/pages/user/user-home.vue    |   2 +-
 .../desktop/views/pages/user/user-photos.vue  |   2 +-
 .../desktop/views/pages/user/user-profile.vue |  10 +-
 src/web/app/desktop/views/pages/user/user.vue |   2 +-
 src/web/app/init.ts                           |  49 +++---
 src/web/app/mobile/views/components/drive.vue |  26 ++--
 .../mobile/views/components/follow-button.vue |  10 +-
 .../mobile/views/components/friends-maker.vue |   2 +-
 .../mobile/views/components/notifications.vue |  10 +-
 .../mobile/views/components/post-detail.vue   |   6 +-
 .../mobile/views/components/posts-post.vue    |  12 +-
 .../views/components/sub-post-content.vue     |   2 +-
 .../app/mobile/views/components/timeline.vue  |  12 +-
 .../app/mobile/views/components/ui-header.vue |  14 +-
 .../app/mobile/views/components/ui-nav.vue    |  16 +-
 src/web/app/mobile/views/components/ui.vue    |  12 +-
 .../views/components/user-followers.vue       |   2 +-
 .../views/components/user-following.vue       |   2 +-
 .../mobile/views/components/user-timeline.vue |   2 +-
 .../mobile/views/components/users-list.vue    |   2 +-
 src/web/app/mobile/views/pages/followers.vue  |   2 +-
 src/web/app/mobile/views/pages/following.vue  |   2 +-
 src/web/app/mobile/views/pages/home.vue       |   8 +-
 .../app/mobile/views/pages/notification.vue   |   2 +-
 src/web/app/mobile/views/pages/post.vue       |   2 +-
 src/web/app/mobile/views/pages/search.vue     |   4 +-
 src/web/app/mobile/views/pages/user.vue       |   4 +-
 .../views/pages/user/followers-you-know.vue   |   2 +-
 .../mobile/views/pages/user/home-activity.vue |   2 +-
 .../mobile/views/pages/user/home-friends.vue  |   2 +-
 .../mobile/views/pages/user/home-photos.vue   |   2 +-
 .../mobile/views/pages/user/home-posts.vue    |   2 +-
 src/web/app/mobile/views/pages/user/home.vue  |   2 +-
 103 files changed, 878 insertions(+), 790 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/drive/browser-window.tag
 delete mode 100644 src/web/app/desktop/-tags/drive/file-contextmenu.tag
 delete mode 100644 src/web/app/desktop/-tags/drive/folder-contextmenu.tag
 create mode 100644 src/web/app/desktop/api/choose-drive-file.ts
 create mode 100644 src/web/app/desktop/api/contextmenu.ts
 create mode 100644 src/web/app/desktop/api/dialog.ts
 create mode 100644 src/web/app/desktop/api/input.ts
 create mode 100644 src/web/app/desktop/views/components/context-menu-menu.vue
 create mode 100644 src/web/app/desktop/views/components/context-menu.vue
 delete mode 100644 src/web/app/desktop/views/components/contextmenu.vue
 delete mode 100644 src/web/app/desktop/views/components/drive-contextmenu.vue
 create mode 100644 src/web/app/desktop/views/components/drive-window.vue

diff --git a/src/web/app/common/views/components/messaging-form.vue b/src/web/app/common/views/components/messaging-form.vue
index bf4dd17ba..18d45790e 100644
--- a/src/web/app/common/views/components/messaging-form.vue
+++ b/src/web/app/common/views/components/messaging-form.vue
@@ -62,7 +62,7 @@ export default Vue.extend({
 
 		send() {
 			this.sending = true;
-			this.$root.$data.os.api('messaging/messages/create', {
+			(this as any).api('messaging/messages/create', {
 				user_id: this.user.id,
 				text: this.text
 			}).then(message => {
diff --git a/src/web/app/common/views/components/messaging-message.vue b/src/web/app/common/views/components/messaging-message.vue
index b1afe7a69..6f44332af 100644
--- a/src/web/app/common/views/components/messaging-message.vue
+++ b/src/web/app/common/views/components/messaging-message.vue
@@ -8,7 +8,7 @@
 			<p class="read" v-if="message.is_me && message.is_read">%i18n:common.tags.mk-messaging-message.is-read%</p>
 			<button class="delete-button" v-if="message.is_me" title="%i18n:common.delete%"><img src="/assets/desktop/messaging/delete.png" alt="Delete"/></button>
 			<div class="content" v-if="!message.is_deleted">
-				<mk-post-html v-if="message.ast" :ast="message.ast" :i="$root.$data.os.i"/>
+				<mk-post-html v-if="message.ast" :ast="message.ast" :i="os.i"/>
 				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 				<div class="image" v-if="message.file"><img src={ message.file.url } alt="image" title={ message.file.name }/></div>
 			</div>
@@ -30,7 +30,7 @@ export default Vue.extend({
 	props: ['message'],
 	computed: {
 		isMe(): boolean {
-			return this.message.user_id == this.$root.$data.os.i.id;
+			return this.message.user_id == (this as any).os.i.id;
 		},
 		urls(): string[] {
 			if (this.message.ast) {
diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index 838e1e265..978610d7f 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -48,7 +48,7 @@ export default Vue.extend({
 	},
 
 	mounted() {
-		this.connection = new MessagingStreamConnection(this.$root.$data.os.i, this.user.id);
+		this.connection = new MessagingStreamConnection((this as any).os.i, this.user.id);
 
 		this.connection.on('message', this.onMessage);
 		this.connection.on('read', this.onRead);
@@ -72,7 +72,7 @@ export default Vue.extend({
 			return new Promise((resolve, reject) => {
 				const max = this.existMoreMessages ? 20 : 10;
 
-				this.$root.$data.os.api('messaging/messages', {
+				(this as any).api('messaging/messages', {
 					user_id: this.user.id,
 					limit: max + 1,
 					until_id: this.existMoreMessages ? this.messages[0].id : undefined
@@ -99,7 +99,7 @@ export default Vue.extend({
 			const isBottom = this.isBottom();
 
 			this.messages.push(message);
-			if (message.user_id != this.$root.$data.os.i.id && !document.hidden) {
+			if (message.user_id != (this as any).os.i.id && !document.hidden) {
 				this.connection.send({
 					type: 'read',
 					id: message.id
@@ -109,7 +109,7 @@ export default Vue.extend({
 			if (isBottom) {
 				// Scroll to bottom
 				this.scrollToBottom();
-			} else if (message.user_id != this.$root.$data.os.i.id) {
+			} else if (message.user_id != (this as any).os.i.id) {
 				// Notify
 				this.notify('%i18n:common.tags.mk-messaging-room.new-message%');
 			}
@@ -157,7 +157,7 @@ export default Vue.extend({
 		onVisibilitychange() {
 			if (document.hidden) return;
 			this.messages.forEach(message => {
-				if (message.user_id !== this.$root.$data.os.i.id && !message.is_read) {
+				if (message.user_id !== (this as any).os.i.id && !message.is_read) {
 					this.connection.send({
 						type: 'read',
 						id: message.id
diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index f45f99b53..1b56382b0 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -71,13 +71,13 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.connection = this.$root.$data.os.streams.messagingIndexStream.getConnection();
-		this.connectionId = this.$root.$data.os.streams.messagingIndexStream.use();
+		this.connection = (this as any).os.streams.messagingIndexStream.getConnection();
+		this.connectionId = (this as any).os.streams.messagingIndexStream.use();
 
 		this.connection.on('message', this.onMessage);
 		this.connection.on('read', this.onRead);
 
-		this.$root.$data.os.api('messaging/history').then(messages => {
+		(this as any).api('messaging/history').then(messages => {
 			this.fetching = false;
 			this.messages = messages;
 		});
@@ -85,11 +85,11 @@ export default Vue.extend({
 	beforeDestroy() {
 		this.connection.off('message', this.onMessage);
 		this.connection.off('read', this.onRead);
-		this.$root.$data.os.stream.dispose(this.connectionId);
+		(this as any).os.stream.dispose(this.connectionId);
 	},
 	methods: {
 		isMe(message) {
-			return message.user_id == this.$root.$data.os.i.id;
+			return message.user_id == (this as any).os.i.id;
 		},
 		onMessage(message) {
 			this.messages = this.messages.filter(m => !(
@@ -109,7 +109,7 @@ export default Vue.extend({
 				this.result = [];
 				return;
 			}
-			this.$root.$data.os.api('users/search', {
+			(this as any).api('users/search', {
 				query: this.q,
 				max: 5
 			}).then(users => {
diff --git a/src/web/app/common/views/components/poll.vue b/src/web/app/common/views/components/poll.vue
index 19ce557e7..d06c019db 100644
--- a/src/web/app/common/views/components/poll.vue
+++ b/src/web/app/common/views/components/poll.vue
@@ -47,7 +47,7 @@
 			},
 			vote(id) {
 				if (this.poll.choices.some(c => c.is_voted)) return;
-				this.$root.$data.os.api('posts/polls/vote', {
+				(this as any).api('posts/polls/vote', {
 					post_id: this.post.id,
 					choice: id
 				}).then(() => {
diff --git a/src/web/app/common/views/components/post-menu.vue b/src/web/app/common/views/components/post-menu.vue
index 7a33360f6..e14d67fc8 100644
--- a/src/web/app/common/views/components/post-menu.vue
+++ b/src/web/app/common/views/components/post-menu.vue
@@ -48,7 +48,7 @@ export default Vue.extend({
 	},
 	methods: {
 		pin() {
-			this.$root.$data.os.api('i/pin', {
+			(this as any).api('i/pin', {
 				post_id: this.post.id
 			}).then(() => {
 				this.$destroy();
diff --git a/src/web/app/common/views/components/reaction-picker.vue b/src/web/app/common/views/components/reaction-picker.vue
index 0446d7b18..f3731cd63 100644
--- a/src/web/app/common/views/components/reaction-picker.vue
+++ b/src/web/app/common/views/components/reaction-picker.vue
@@ -68,7 +68,7 @@ export default Vue.extend({
 	},
 	methods: {
 		react(reaction) {
-			this.$root.$data.os.api('posts/reactions/create', {
+			(this as any).api('posts/reactions/create', {
 				post_id: this.post.id,
 				reaction: reaction
 			}).then(() => {
diff --git a/src/web/app/common/views/components/signin.vue b/src/web/app/common/views/components/signin.vue
index 989c01705..31243e99a 100644
--- a/src/web/app/common/views/components/signin.vue
+++ b/src/web/app/common/views/components/signin.vue
@@ -28,7 +28,7 @@ export default Vue.extend({
 	},
 	methods: {
 		onUsernameChange() {
-			this.$root.$data.os.api('users/show', {
+			(this as any).api('users/show', {
 				username: this.username
 			}).then(user => {
 				this.user = user;
@@ -37,7 +37,7 @@ export default Vue.extend({
 		onSubmit() {
 			this.signing = true;
 
-			this.$root.$data.os.api('signin', {
+			(this as any).api('signin', {
 				username: this.username,
 				password: this.password,
 				token: this.user && this.user.two_factor_enabled ? this.token : undefined
diff --git a/src/web/app/common/views/components/signup.vue b/src/web/app/common/views/components/signup.vue
index 34d17ef0e..1fdc49a18 100644
--- a/src/web/app/common/views/components/signup.vue
+++ b/src/web/app/common/views/components/signup.vue
@@ -88,7 +88,7 @@ export default Vue.extend({
 
 			this.usernameState = 'wait';
 
-			this.$root.$data.os.api('username/available', {
+			(this as any).api('username/available', {
 				username: this.username
 			}).then(result => {
 				this.usernameState = result.available ? 'ok' : 'unavailable';
@@ -115,12 +115,12 @@ export default Vue.extend({
 			this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match';
 		},
 		onSubmit() {
-			this.$root.$data.os.api('signup', {
+			(this as any).api('signup', {
 				username: this.username,
 				password: this.password,
 				'g-recaptcha-response': (window as any).grecaptcha.getResponse()
 			}).then(() => {
-				this.$root.$data.os.api('signin', {
+				(this as any).api('signin', {
 					username: this.username,
 					password: this.password
 				}).then(() => {
diff --git a/src/web/app/common/views/components/stream-indicator.vue b/src/web/app/common/views/components/stream-indicator.vue
index 00bd58c1f..c1c0672e4 100644
--- a/src/web/app/common/views/components/stream-indicator.vue
+++ b/src/web/app/common/views/components/stream-indicator.vue
@@ -26,10 +26,10 @@ export default Vue.extend({
 		};
 	},
 	created() {
-		this.stream = this.$root.$data.os.stream.borrow();
+		this.stream = (this as any).os.stream.borrow();
 
-		this.$root.$data.os.stream.on('connected', this.onConnected);
-		this.$root.$data.os.stream.on('disconnected', this.onDisconnected);
+		(this as any).os.stream.on('connected', this.onConnected);
+		(this as any).os.stream.on('disconnected', this.onDisconnected);
 
 		this.$nextTick(() => {
 			if (this.stream.state == 'connected') {
@@ -38,12 +38,12 @@ export default Vue.extend({
 		});
 	},
 	beforeDestroy() {
-		this.$root.$data.os.stream.off('connected', this.onConnected);
-		this.$root.$data.os.stream.off('disconnected', this.onDisconnected);
+		(this as any).os.stream.off('connected', this.onConnected);
+		(this as any).os.stream.off('disconnected', this.onDisconnected);
 	},
 	methods: {
 		onConnected() {
-			this.stream = this.$root.$data.os.stream.borrow();
+			this.stream = (this as any).os.stream.borrow();
 
 			setTimeout(() => {
 				anime({
diff --git a/src/web/app/common/views/components/uploader.vue b/src/web/app/common/views/components/uploader.vue
index 21f92caab..6367b6997 100644
--- a/src/web/app/common/views/components/uploader.vue
+++ b/src/web/app/common/views/components/uploader.vue
@@ -50,7 +50,7 @@ export default Vue.extend({
 			reader.readAsDataURL(file);
 
 			const data = new FormData();
-			data.append('i', this.$root.$data.os.i.token);
+			data.append('i', (this as any).os.i.token);
 			data.append('file', file);
 
 			if (folder) data.append('folder_id', folder);
diff --git a/src/web/app/common/views/components/widgets/photo-stream.vue b/src/web/app/common/views/components/widgets/photo-stream.vue
index afbdc2162..4d6b66069 100644
--- a/src/web/app/common/views/components/widgets/photo-stream.vue
+++ b/src/web/app/common/views/components/widgets/photo-stream.vue
@@ -26,12 +26,12 @@ export default define({
 		};
 	},
 	mounted() {
-		this.connection = this.$root.$data.os.stream.getConnection();
-		this.connectionId = this.$root.$data.os.stream.use();
+		this.connection = (this as any).os.stream.getConnection();
+		this.connectionId = (this as any).os.stream.use();
 
 		this.connection.on('drive_file_created', this.onDriveFileCreated);
 
-		this.$root.$data.os.api('drive/stream', {
+		(this as any).api('drive/stream', {
 			type: 'image/*',
 			limit: 9
 		}).then(images => {
@@ -41,7 +41,7 @@ export default define({
 	},
 	beforeDestroy() {
 		this.connection.off('drive_file_created', this.onDriveFileCreated);
-		this.$root.$data.os.stream.dispose(this.connectionId);
+		(this as any).os.stream.dispose(this.connectionId);
 	},
 	methods: {
 		onDriveFileCreated(file) {
diff --git a/src/web/app/common/views/components/widgets/slideshow.vue b/src/web/app/common/views/components/widgets/slideshow.vue
index c24e3003c..a200aa061 100644
--- a/src/web/app/common/views/components/widgets/slideshow.vue
+++ b/src/web/app/common/views/components/widgets/slideshow.vue
@@ -89,7 +89,7 @@ export default define({
 		fetch() {
 			this.fetching = true;
 
-			this.$root.$data.os.api('drive/files', {
+			(this as any).api('drive/files', {
 				folder_id: this.props.folder,
 				type: 'image/*',
 				limit: 100
@@ -102,7 +102,7 @@ export default define({
 			});
 		},
 		choose() {
-			this.$root.$data.api.chooseDriveFolder().then(folder => {
+			(this as any).apis.chooseDriveFolder().then(folder => {
 				this.props.folder = folder ? folder.id : null;
 				this.fetch();
 			});
diff --git a/src/web/app/desktop/-tags/drive/browser-window.tag b/src/web/app/desktop/-tags/drive/browser-window.tag
deleted file mode 100644
index c9c765252..000000000
--- a/src/web/app/desktop/-tags/drive/browser-window.tag
+++ /dev/null
@@ -1,60 +0,0 @@
-<mk-drive-browser-window>
-	<mk-window ref="window" is-modal={ false } width={ '800px' } height={ '500px' } popout={ popout }>
-		<yield to="header">
-			<p class="info" v-if="parent.usage"><b>{ parent.usage.toFixed(1) }%</b> %i18n:desktop.tags.mk-drive-browser-window.used%</p>
-			%fa:cloud%%i18n:desktop.tags.mk-drive-browser-window.drive%
-		</yield>
-		<yield to="content">
-			<mk-drive-browser multiple={ true } folder={ parent.folder } ref="browser"/>
-		</yield>
-	</mk-window>
-	<style lang="stylus" scoped>
-		:scope
-			> mk-window
-				[data-yield='header']
-					> .info
-						position absolute
-						top 0
-						left 16px
-						margin 0
-						font-size 80%
-
-					> [data-fa]
-						margin-right 4px
-
-				[data-yield='content']
-					> mk-drive-browser
-						height 100%
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.folder = this.opts.folder ? this.opts.folder : null;
-
-		this.popout = () => {
-			const folder = this.$refs.window.refs.browser.folder;
-			if (folder) {
-				return `${_URL_}/i/drive/folder/${folder.id}`;
-			} else {
-				return `${_URL_}/i/drive`;
-			}
-		};
-
-		this.on('mount', () => {
-			this.$refs.window.on('closed', () => {
-				this.$destroy();
-			});
-
-			this.$root.$data.os.api('drive').then(info => {
-				this.update({
-					usage: info.usage / info.capacity * 100
-				});
-			});
-		});
-
-		this.close = () => {
-			this.$refs.window.close();
-		};
-	</script>
-</mk-drive-browser-window>
diff --git a/src/web/app/desktop/-tags/drive/file-contextmenu.tag b/src/web/app/desktop/-tags/drive/file-contextmenu.tag
deleted file mode 100644
index 8776fcc02..000000000
--- a/src/web/app/desktop/-tags/drive/file-contextmenu.tag
+++ /dev/null
@@ -1,99 +0,0 @@
-<mk-drive-browser-file-contextmenu>
-	<mk-contextmenu ref="ctx">
-		<ul>
-			<li @click="parent.rename">
-				<p>%fa:i-cursor%%i18n:desktop.tags.mk-drive-browser-file-contextmenu.rename%</p>
-			</li>
-			<li @click="parent.copyUrl">
-				<p>%fa:link%%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copy-url%</p>
-			</li>
-			<li><a href={ parent.file.url + '?download' } download={ parent.file.name } @click="parent.download">%fa:download%%i18n:desktop.tags.mk-drive-browser-file-contextmenu.download%</a></li>
-			<li class="separator"></li>
-			<li @click="parent.delete">
-				<p>%fa:R trash-alt%%i18n:common.delete%</p>
-			</li>
-			<li class="separator"></li>
-			<li class="has-child">
-				<p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.else-files%%fa:caret-right%</p>
-				<ul>
-					<li @click="parent.setAvatar">
-						<p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.set-as-avatar%</p>
-					</li>
-					<li @click="parent.setBanner">
-						<p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.set-as-banner%</p>
-					</li>
-				</ul>
-			</li>
-			<li class="has-child">
-				<p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.open-in-app%...%fa:caret-right%</p>
-				<ul>
-					<li @click="parent.addApp">
-						<p>%i18n:desktop.tags.mk-drive-browser-file-contextmenu.add-app%...</p>
-					</li>
-				</ul>
-			</li>
-		</ul>
-	</mk-contextmenu>
-	<script lang="typescript">
-		import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
-		import dialog from '../../scripts/dialog';
-		import inputDialog from '../../scripts/input-dialog';
-		import updateAvatar from '../../scripts/update-avatar';
-		import NotImplementedException from '../../scripts/not-implemented-exception';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.browser = this.opts.browser;
-		this.file = this.opts.file;
-
-		this.on('mount', () => {
-			this.$refs.ctx.on('closed', () => {
-				this.$emit('closed');
-				this.$destroy();
-			});
-		});
-
-		this.open = pos => {
-			this.$refs.ctx.open(pos);
-		};
-
-		this.rename = () => {
-			this.$refs.ctx.close();
-
-			inputDialog('%i18n:desktop.tags.mk-drive-browser-file-contextmenu.rename-file%', '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.input-new-file-name%', this.file.name, name => {
-				this.$root.$data.os.api('drive/files/update', {
-					file_id: this.file.id,
-					name: name
-				})
-			});
-		};
-
-		this.copyUrl = () => {
-			copyToClipboard(this.file.url);
-			this.$refs.ctx.close();
-			dialog('%fa:check%%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copied%',
-				'%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copied-url-to-clipboard%', [{
-				text: '%i18n:common.ok%'
-			}]);
-		};
-
-		this.download = () => {
-			this.$refs.ctx.close();
-		};
-
-		this.setAvatar = () => {
-			this.$refs.ctx.close();
-			updateAvatar(this.I, null, this.file);
-		};
-
-		this.setBanner = () => {
-			this.$refs.ctx.close();
-			updateBanner(this.I, null, this.file);
-		};
-
-		this.addApp = () => {
-			NotImplementedException();
-		};
-	</script>
-</mk-drive-browser-file-contextmenu>
diff --git a/src/web/app/desktop/-tags/drive/folder-contextmenu.tag b/src/web/app/desktop/-tags/drive/folder-contextmenu.tag
deleted file mode 100644
index a0146410f..000000000
--- a/src/web/app/desktop/-tags/drive/folder-contextmenu.tag
+++ /dev/null
@@ -1,63 +0,0 @@
-<mk-drive-browser-folder-contextmenu>
-	<mk-contextmenu ref="ctx">
-		<ul>
-			<li @click="parent.move">
-				<p>%fa:arrow-right%%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.move-to-this-folder%</p>
-			</li>
-			<li @click="parent.newWindow">
-				<p>%fa:R window-restore%%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.show-in-new-window%</p>
-			</li>
-			<li class="separator"></li>
-			<li @click="parent.rename">
-				<p>%fa:i-cursor%%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.rename%</p>
-			</li>
-			<li class="separator"></li>
-			<li @click="parent.delete">
-				<p>%fa:R trash-alt%%i18n:common.delete%</p>
-			</li>
-		</ul>
-	</mk-contextmenu>
-	<script lang="typescript">
-		import inputDialog from '../../scripts/input-dialog';
-
-		this.mixin('api');
-
-		this.browser = this.opts.browser;
-		this.folder = this.opts.folder;
-
-		this.open = pos => {
-			this.$refs.ctx.open(pos);
-
-			this.$refs.ctx.on('closed', () => {
-				this.$emit('closed');
-				this.$destroy();
-			});
-		};
-
-		this.move = () => {
-			this.browser.move(this.folder.id);
-			this.$refs.ctx.close();
-		};
-
-		this.newWindow = () => {
-			this.browser.newWindow(this.folder.id);
-			this.$refs.ctx.close();
-		};
-
-		this.createFolder = () => {
-			this.browser.createFolder();
-			this.$refs.ctx.close();
-		};
-
-		this.rename = () => {
-			this.$refs.ctx.close();
-
-			inputDialog('%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.rename-folder%', '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.input-new-folder-name%', this.folder.name, name => {
-				this.$root.$data.os.api('drive/folders/update', {
-					folder_id: this.folder.id,
-					name: name
-				});
-			});
-		};
-	</script>
-</mk-drive-browser-folder-contextmenu>
diff --git a/src/web/app/desktop/api/choose-drive-file.ts b/src/web/app/desktop/api/choose-drive-file.ts
new file mode 100644
index 000000000..e04844171
--- /dev/null
+++ b/src/web/app/desktop/api/choose-drive-file.ts
@@ -0,0 +1,18 @@
+import MkChooseFileFromDriveWindow from '../views/components/choose-file-from-drive-window.vue';
+
+export default function(opts) {
+	return new Promise((res, rej) => {
+		const o = opts || {};
+		const w = new MkChooseFileFromDriveWindow({
+			propsData: {
+				title: o.title,
+				multiple: o.multiple,
+				initFolder: o.currentFolder
+			}
+		}).$mount();
+		w.$once('selected', file => {
+			res(file);
+		});
+		document.body.appendChild(w.$el);
+	});
+}
diff --git a/src/web/app/desktop/api/choose-drive-folder.ts b/src/web/app/desktop/api/choose-drive-folder.ts
index a5116f7bc..9b33a20d9 100644
--- a/src/web/app/desktop/api/choose-drive-folder.ts
+++ b/src/web/app/desktop/api/choose-drive-folder.ts
@@ -1,12 +1,12 @@
-import MkChooseFolderFromDriveWindow from '../../../common/views/components/choose-folder-from-drive-window.vue';
+import MkChooseFolderFromDriveWindow from '../views/components/choose-folder-from-drive-window.vue';
 
-export default function(this: any, opts) {
+export default function(opts) {
 	return new Promise((res, rej) => {
 		const o = opts || {};
 		const w = new MkChooseFolderFromDriveWindow({
-			parent: this,
 			propsData: {
-				title: o.title
+				title: o.title,
+				initFolder: o.currentFolder
 			}
 		}).$mount();
 		w.$once('selected', folder => {
diff --git a/src/web/app/desktop/api/contextmenu.ts b/src/web/app/desktop/api/contextmenu.ts
new file mode 100644
index 000000000..b70d7122d
--- /dev/null
+++ b/src/web/app/desktop/api/contextmenu.ts
@@ -0,0 +1,16 @@
+import Ctx from '../views/components/context-menu.vue';
+
+export default function(e, menu, opts?) {
+	const o = opts || {};
+	const vm = new Ctx({
+		propsData: {
+			menu,
+			x: e.pageX - window.pageXOffset,
+			y: e.pageY - window.pageYOffset,
+		}
+	}).$mount();
+	vm.$once('closed', () => {
+		if (o.closed) o.closed();
+	});
+	document.body.appendChild(vm.$el);
+}
diff --git a/src/web/app/desktop/api/dialog.ts b/src/web/app/desktop/api/dialog.ts
new file mode 100644
index 000000000..07935485b
--- /dev/null
+++ b/src/web/app/desktop/api/dialog.ts
@@ -0,0 +1,19 @@
+import Dialog from '../views/components/dialog.vue';
+
+export default function(opts) {
+	return new Promise<string>((res, rej) => {
+		const o = opts || {};
+		const d = new Dialog({
+			propsData: {
+				title: o.title,
+				text: o.text,
+				modal: o.modal,
+				buttons: o.actions
+			}
+		}).$mount();
+		d.$once('clicked', id => {
+			res(id);
+		});
+		document.body.appendChild(d.$el);
+	});
+}
diff --git a/src/web/app/desktop/api/input.ts b/src/web/app/desktop/api/input.ts
new file mode 100644
index 000000000..a5ab07138
--- /dev/null
+++ b/src/web/app/desktop/api/input.ts
@@ -0,0 +1,19 @@
+import InputDialog from '../views/components/input-dialog.vue';
+
+export default function(opts) {
+	return new Promise<string>((res, rej) => {
+		const o = opts || {};
+		const d = new InputDialog({
+			propsData: {
+				title: o.title,
+				placeholder: o.placeholder,
+				default: o.default,
+				type: o.type || 'text'
+			}
+		}).$mount();
+		d.$once('done', text => {
+			res(text);
+		});
+		document.body.appendChild(d.$el);
+	});
+}
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index cd894170e..cb7a53fb2 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -11,6 +11,9 @@ import HomeStreamManager from '../common/scripts/streaming/home-stream-manager';
 import composeNotification from '../common/scripts/compose-notification';
 
 import chooseDriveFolder from './api/choose-drive-folder';
+import chooseDriveFile from './api/choose-drive-file';
+import dialog from './api/dialog';
+import input from './api/input';
 
 import MkIndex from './views/pages/index.vue';
 
@@ -30,7 +33,10 @@ init(async (launch) => {
 	require('./views/components');
 
 	const app = launch({
-		chooseDriveFolder
+		chooseDriveFolder,
+		chooseDriveFile,
+		dialog,
+		input
 	});
 
 	/**
diff --git a/src/web/app/desktop/views/components/2fa-setting.vue b/src/web/app/desktop/views/components/2fa-setting.vue
index 146d707e1..8271cbbf3 100644
--- a/src/web/app/desktop/views/components/2fa-setting.vue
+++ b/src/web/app/desktop/views/components/2fa-setting.vue
@@ -36,7 +36,7 @@ export default Vue.extend({
 	methods: {
 		register() {
 			passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => {
-				this.$root.$data.os.api('i/2fa/register', {
+				(this as any).api('i/2fa/register', {
 					password: password
 				}).then(data => {
 					this.data = data;
@@ -46,21 +46,21 @@ export default Vue.extend({
 
 		unregister() {
 			passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => {
-				this.$root.$data.os.api('i/2fa/unregister', {
+				(this as any).api('i/2fa/unregister', {
 					password: password
 				}).then(() => {
 					notify('%i18n:desktop.tags.mk-2fa-setting.unregistered%');
-					this.$root.$data.os.i.two_factor_enabled = false;
+					(this as any).os.i.two_factor_enabled = false;
 				});
 			});
 		},
 
 		submit() {
-			this.$root.$data.os.api('i/2fa/done', {
+			(this as any).api('i/2fa/done', {
 				token: this.token
 			}).then(() => {
 				notify('%i18n:desktop.tags.mk-2fa-setting.success%');
-				this.$root.$data.os.i.two_factor_enabled = true;
+				(this as any).os.i.two_factor_enabled = true;
 			}).catch(() => {
 				notify('%i18n:desktop.tags.mk-2fa-setting.failed%');
 			});
diff --git a/src/web/app/desktop/views/components/api-setting.vue b/src/web/app/desktop/views/components/api-setting.vue
index 78429064b..08c5a0c51 100644
--- a/src/web/app/desktop/views/components/api-setting.vue
+++ b/src/web/app/desktop/views/components/api-setting.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-api-setting">
-	<p>Token: <code>{{ $root.$data.os.i.token }}</code></p>
+	<p>Token: <code>{{ os.i.token }}</code></p>
 	<p>%i18n:desktop.tags.mk-api-info.intro%</p>
 	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-api-info.caution%</p></div>
 	<p>%i18n:desktop.tags.mk-api-info.regeneration-of-token%</p>
@@ -16,7 +16,7 @@ export default Vue.extend({
 	methods: {
 		regenerateToken() {
 			passwordDialog('%i18n:desktop.tags.mk-api-info.enter-password%', password => {
-				this.$root.$data.os.api('i/regenerate_token', {
+				(this as any).api('i/regenerate_token', {
 					password: password
 				});
 			});
diff --git a/src/web/app/desktop/views/components/context-menu-menu.vue b/src/web/app/desktop/views/components/context-menu-menu.vue
new file mode 100644
index 000000000..423ea0a1f
--- /dev/null
+++ b/src/web/app/desktop/views/components/context-menu-menu.vue
@@ -0,0 +1,113 @@
+<template>
+<ul class="me-nu">
+	<li v-for="(item, i) in menu" :key="i" :class="item.type">
+		<template v-if="item.type == 'item'">
+			<p @click="click(item)"><span class="icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</p>
+		</template>
+		<template v-else-if="item.type == 'nest'">
+			<p><span class="icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}...<span class="caret">%fa:caret-right%</span></p>
+			<me-nu :menu="item.menu" @x="click"/>
+		</template>
+	</li>
+</ul>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	name: 'me-nu',
+	props: ['menu'],
+	methods: {
+		click(item) {
+			this.$emit('x', item);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.me-nu
+	$width = 240px
+	$item-height = 38px
+	$padding = 10px
+
+	ul
+		display block
+		margin 0
+		padding $padding 0
+		list-style none
+
+	li
+		display block
+
+		&:empty
+			margin-top $padding
+			padding-top $padding
+			border-top solid 1px #eee
+
+		&.nest
+			> p
+				cursor default
+
+				> .caret
+					> *
+						position absolute
+						top 0
+						right 8px
+						line-height $item-height
+
+			&:hover > ul
+				visibility visible
+
+			&:active
+				> p, a
+					background $theme-color
+
+		> p, a
+			display block
+			z-index 1
+			margin 0
+			padding 0 32px 0 38px
+			line-height $item-height
+			color #868C8C
+			text-decoration none
+			cursor pointer
+
+			&:hover
+				text-decoration none
+
+			*
+				pointer-events none
+
+			> .icon
+				> *
+					width 28px
+					margin-left -28px
+					text-align center
+
+		&:hover
+			> p, a
+				text-decoration none
+				background $theme-color
+				color $theme-color-foreground
+
+		&:active
+			> p, a
+				text-decoration none
+				background darken($theme-color, 10%)
+				color $theme-color-foreground
+
+	li > ul
+		visibility hidden
+		position absolute
+		top 0
+		left $width
+		margin-top -($padding)
+		width $width
+		background #fff
+		border-radius 0 4px 4px 4px
+		box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2)
+		transition visibility 0s linear 0.2s
+
+</style>
+
diff --git a/src/web/app/desktop/views/components/context-menu.vue b/src/web/app/desktop/views/components/context-menu.vue
new file mode 100644
index 000000000..9f5787e47
--- /dev/null
+++ b/src/web/app/desktop/views/components/context-menu.vue
@@ -0,0 +1,74 @@
+<template>
+<div class="context-menu" :style="{ x: `${x}px`, y: `${y}px` }" @contextmenu.prevent="() => {}">
+	<me-nu :menu="menu" @x="click"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+import contains from '../../../common/scripts/contains';
+import meNu from './context-menu-menu.vue';
+
+export default Vue.extend({
+	components: {
+		'me-nu': meNu
+	},
+	props: ['x', 'y', 'menu'],
+	mounted() {
+		this.$nextTick(() => {
+			Array.from(document.querySelectorAll('body *')).forEach(el => {
+				el.addEventListener('mousedown', this.onMousedown);
+			});
+
+			this.$el.style.display = 'block';
+
+			anime({
+				targets: this.$el,
+				opacity: [0, 1],
+				duration: 100,
+				easing: 'linear'
+			});
+		});
+	},
+	methods: {
+		onMousedown(e) {
+			e.preventDefault();
+			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
+			return false;
+		},
+		click(item) {
+			if (item.onClick) item.onClick();
+			this.close();
+		},
+		close() {
+			Array.from(document.querySelectorAll('body *')).forEach(el => {
+				el.removeEventListener('mousedown', this.onMousedown);
+			});
+
+			this.$emit('closed');
+			this.$destroy();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.context-menu
+	$width = 240px
+	$item-height = 38px
+	$padding = 10px
+
+	display none
+	position fixed
+	top 0
+	left 0
+	z-index 4096
+	width $width
+	font-size 0.8em
+	background #fff
+	border-radius 0 4px 4px 4px
+	box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2)
+	opacity 0
+
+</style>
diff --git a/src/web/app/desktop/views/components/contextmenu.vue b/src/web/app/desktop/views/components/contextmenu.vue
deleted file mode 100644
index c6fccc22c..000000000
--- a/src/web/app/desktop/views/components/contextmenu.vue
+++ /dev/null
@@ -1,142 +0,0 @@
-<template>
-<div class="mk-contextmenu" @contextmenu.prevent="() => {}">
-	<slot></slot>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import * as anime from 'animejs';
-import contains from '../../../common/scripts/contains';
-
-export default Vue.extend({
-	props: ['x', 'y'],
-	mounted() {
-		document.querySelectorAll('body *').forEach(el => {
-			el.addEventListener('mousedown', this.onMousedown);
-		});
-
-		this.$el.style.display = 'block';
-		this.$el.style.left = this.x + 'px';
-		this.$el.style.top = this.y + 'px';
-
-		anime({
-			targets: this.$el,
-			opacity: [0, 1],
-			duration: 100,
-			easing: 'linear'
-		});
-	},
-	methods: {
-		onMousedown(e) {
-			e.preventDefault();
-			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
-			return false;
-		},
-		close() {
-			Array.from(document.querySelectorAll('body *')).forEach(el => {
-				el.removeEventListener('mousedown', this.onMousedown);
-			});
-
-			this.$emit('closed');
-			this.$destroy();
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-contextmenu
-	$width = 240px
-	$item-height = 38px
-	$padding = 10px
-
-	display none
-	position fixed
-	top 0
-	left 0
-	z-index 4096
-	width $width
-	font-size 0.8em
-	background #fff
-	border-radius 0 4px 4px 4px
-	box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2)
-	opacity 0
-
-	ul
-		display block
-		margin 0
-		padding $padding 0
-		list-style none
-
-	li
-		display block
-
-		&.separator
-			margin-top $padding
-			padding-top $padding
-			border-top solid 1px #eee
-
-		&.has-child
-			> p
-				cursor default
-
-				> [data-fa]:last-child
-					position absolute
-					top 0
-					right 8px
-					line-height $item-height
-
-			&:hover > ul
-				visibility visible
-
-			&:active
-				> p, a
-					background $theme-color
-
-		> p, a
-			display block
-			z-index 1
-			margin 0
-			padding 0 32px 0 38px
-			line-height $item-height
-			color #868C8C
-			text-decoration none
-			cursor pointer
-
-			&:hover
-				text-decoration none
-
-			*
-				pointer-events none
-
-			> i
-				width 28px
-				margin-left -28px
-				text-align center
-
-		&:hover
-			> p, a
-				text-decoration none
-				background $theme-color
-				color $theme-color-foreground
-
-		&:active
-			> p, a
-				text-decoration none
-				background darken($theme-color, 10%)
-				color $theme-color-foreground
-
-	li > ul
-		visibility hidden
-		position absolute
-		top 0
-		left $width
-		margin-top -($padding)
-		width $width
-		background #fff
-		border-radius 0 4px 4px 4px
-		box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2)
-		transition visibility 0s linear 0.2s
-
-</style>
diff --git a/src/web/app/desktop/views/components/dialog.vue b/src/web/app/desktop/views/components/dialog.vue
index 9bb7fca1b..f2be5e443 100644
--- a/src/web/app/desktop/views/components/dialog.vue
+++ b/src/web/app/desktop/views/components/dialog.vue
@@ -5,7 +5,7 @@
 		<header v-html="title"></header>
 		<div class="body" v-html="text"></div>
 		<div class="buttons">
-			<button v-for="(button, i) in buttons" @click="click(button)" :key="i">{{ button.text }}</button>
+			<button v-for="button in buttons" @click="click(button)" :key="button.id">{{ button.text }}</button>
 		</div>
 	</div>
 </div>
@@ -26,13 +26,9 @@ export default Vue.extend({
 		buttons: {
 			type: Array
 		},
-		canThrough: {
+		modal: {
 			type: Boolean,
-			default: true
-		},
-		onThrough: {
-			type: Function,
-			required: false
+			default: false
 		}
 	},
 	mounted() {
@@ -54,7 +50,7 @@ export default Vue.extend({
 	},
 	methods: {
 		click(button) {
-			if (button.onClick) button.onClick();
+			this.$emit('clicked', button.id);
 			this.close();
 		},
 		close() {
@@ -77,8 +73,7 @@ export default Vue.extend({
 			});
 		},
 		onBgClick() {
-			if (this.canThrough) {
-				if (this.onThrough) this.onThrough();
+			if (!this.modal) {
 				this.close();
 			}
 		}
diff --git a/src/web/app/desktop/views/components/drive-contextmenu.vue b/src/web/app/desktop/views/components/drive-contextmenu.vue
deleted file mode 100644
index bdb3bd00d..000000000
--- a/src/web/app/desktop/views/components/drive-contextmenu.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-<template>
-<mk-contextmenu ref="menu" @closed="onClosed">
-	<ul>
-		<li @click="createFolder">
-			<p>%fa:R folder%%i18n:desktop.tags.mk-drive-browser-base-contextmenu.create-folder%</p>
-		</li>
-		<li @click="upload">
-			<p>%fa:upload%%i18n:desktop.tags.mk-drive-browser-base-contextmenu.upload%</p>
-		</li>
-		<li @click="urlUpload">
-			<p>%fa:cloud-upload-alt%%i18n:desktop.tags.mk-drive-browser-base-contextmenu.url-upload%</p>
-		</li>
-	</ul>
-</mk-contextmenu>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	props: ['browser'],
-	mounted() {
-
-	},
-	methods: {
-		close() {
-			(this.$refs.menu as any).close();
-		},
-		onClosed() {
-			this.$emit('closed');
-			this.$destroy();
-		},
-		createFolder() {
-			this.browser.createFolder();
-			this.close();
-		},
-		upload() {
-			this.browser.selectLocalFile();
-			this.close();
-		},
-		urlUpload() {
-			this.browser.urlUpload();
-			this.close();
-		}
-	}
-});
-</script>
diff --git a/src/web/app/desktop/views/components/drive-file.vue b/src/web/app/desktop/views/components/drive-file.vue
index cda561d31..0681b5f03 100644
--- a/src/web/app/desktop/views/components/drive-file.vue
+++ b/src/web/app/desktop/views/components/drive-file.vue
@@ -3,24 +3,24 @@
 	:data-is-selected="isSelected"
 	:data-is-contextmenu-showing="isContextmenuShowing"
 	@click="onClick"
-	@contextmenu.prevent.stop="onContextmenu"
 	draggable="true"
 	@dragstart="onDragstart"
 	@dragend="onDragend"
+	@contextmenu.prevent.stop="onContextmenu"
 	:title="title"
 >
-	<div class="label" v-if="I.avatar_id == file.id"><img src="/assets/label.svg"/>
+	<div class="label" v-if="os.i.avatar_id == file.id"><img src="/assets/label.svg"/>
 		<p>%i18n:desktop.tags.mk-drive-browser-file.avatar%</p>
 	</div>
-	<div class="label" v-if="I.banner_id == file.id"><img src="/assets/label.svg"/>
+	<div class="label" v-if="os.i.banner_id == file.id"><img src="/assets/label.svg"/>
 		<p>%i18n:desktop.tags.mk-drive-browser-file.banner%</p>
 	</div>
-	<div class="thumbnail" ref="thumbnail" style="background-color:{ file.properties.average_color ? 'rgb(' + file.properties.average_color.join(',') + ')' : 'transparent' }">
-		<img src={ file.url + '?thumbnail&size=128' } alt="" @load="onThumbnailLoaded"/>
+	<div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`">
+		<img :src="`${file.url}?thumbnail&size=128`" alt="" @load="onThumbnailLoaded"/>
 	</div>
 	<p class="name">
-		<span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span>
-		<span class="ext" v-if="file.name.lastIndexOf('.') != -1">{ file.name.substr(file.name.lastIndexOf('.')) }</span>
+		<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
+		<span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
 	</p>
 </div>
 </template>
@@ -28,10 +28,12 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as anime from 'animejs';
+import contextmenu from '../../api/contextmenu';
+import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
 import bytesToSize from '../../../common/scripts/bytes-to-size';
 
 export default Vue.extend({
-	props: ['file', 'browser'],
+	props: ['file'],
 	data() {
 		return {
 			isContextmenuShowing: false,
@@ -39,11 +41,19 @@ export default Vue.extend({
 		};
 	},
 	computed: {
+		browser(): any {
+			return this.$parent;
+		},
 		isSelected(): boolean {
 			return this.browser.selectedFiles.some(f => f.id == this.file.id);
 		},
 		title(): string {
 			return `${this.file.name}\n${this.file.type} ${bytesToSize(this.file.datasize)}`;
+		},
+		background(): string {
+			return this.file.properties.average_color
+				? `rgb(${this.file.properties.average_color.join(',')})'`
+				: 'transparent';
 		}
 	},
 	methods: {
@@ -53,18 +63,55 @@ export default Vue.extend({
 
 		onContextmenu(e) {
 			this.isContextmenuShowing = true;
-			const ctx = new MkDriveFileContextmenu({
-				parent: this,
-				propsData: {
-					browser: this.browser,
-					x: e.pageX - window.pageXOffset,
-					y: e.pageY - window.pageYOffset
+			contextmenu(e, [{
+				type: 'item',
+				text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.rename%',
+				icon: '%fa:i-cursor%',
+				onClick: this.rename
+			}, {
+				type: 'item',
+				text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copy-url%',
+				icon: '%fa:link%',
+				onClick: this.copyUrl
+			}, {
+				type: 'link',
+				href: `${this.file.url}?download`,
+				text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.download%',
+				icon: '%fa:download%',
+			}, {
+				type: 'divider',
+			}, {
+				type: 'item',
+				text: '%i18n:common.delete%',
+				icon: '%fa:R trash-alt%',
+				onClick: this.deleteFile
+			}, {
+				type: 'divider',
+			}, {
+				type: 'nest',
+				text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.else-files%',
+				menu: [{
+					type: 'item',
+					text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.set-as-avatar%',
+					onClick: this.setAsAvatar
+				}, {
+					type: 'item',
+					text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.set-as-banner%',
+					onClick: this.setAsBanner
+				}]
+			}, {
+				type: 'nest',
+				text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.open-in-app%',
+				menu: [{
+					type: 'item',
+					text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.add-app%...',
+					onClick: this.addApp
+				}]
+			}], {
+				closed: () => {
+					this.isContextmenuShowing = false;
 				}
-			}).$mount();
-			ctx.$once('closed', () => {
-				this.isContextmenuShowing = false;
 			});
-			document.body.appendChild(ctx.$el);
 		},
 
 		onDragstart(e) {
@@ -95,6 +142,46 @@ export default Vue.extend({
 					easing: 'linear'
 				});
 			}
+		},
+
+		rename() {
+			(this as any).apis.input({
+				title: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.rename-file%',
+				placeholder: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.input-new-file-name%',
+				default: this.file.name
+			}).then(name => {
+				(this as any).api('drive/files/update', {
+					file_id: this.file.id,
+					name: name
+				})
+			});
+		},
+
+		copyUrl() {
+			copyToClipboard(this.file.url);
+			(this as any).apis.dialog({
+				title: '%fa:check%%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copied%',
+				text: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.copied-url-to-clipboard%',
+				actions: [{
+					text: '%i18n:common.ok%'
+				}]
+			});
+		},
+
+		setAsAvatar() {
+			(this as any).apis.updateAvatar(this.file);
+		},
+
+		setAsBanner() {
+			(this as any).apis.updateBanner(this.file);
+		},
+
+		addApp() {
+			alert('not implemented yet');
+		},
+
+		deleteFile() {
+			alert('not implemented yet');
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/drive-folder.vue b/src/web/app/desktop/views/components/drive-folder.vue
index e9e4f1de2..bfb134501 100644
--- a/src/web/app/desktop/views/components/drive-folder.vue
+++ b/src/web/app/desktop/views/components/drive-folder.vue
@@ -9,10 +9,10 @@
 	@dragenter.prevent="onDragenter"
 	@dragleave="onDragleave"
 	@drop.prevent.stop="onDrop"
-	@contextmenu.prevent.stop="onContextmenu"
 	draggable="true"
 	@dragstart="onDragstart"
 	@dragend="onDragend"
+	@contextmenu.prevent.stop="onContextmenu"
 	:title="title"
 >
 	<p class="name">
@@ -25,10 +25,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import dialog from '../../scripts/dialog';
+import contextmenu from '../../api/contextmenu';
 
 export default Vue.extend({
-	props: ['folder', 'browser'],
+	props: ['folder'],
 	data() {
 		return {
 			hover: false,
@@ -38,6 +38,9 @@ export default Vue.extend({
 		};
 	},
 	computed: {
+		browser(): any {
+			return this.$parent;
+		},
 		title(): string {
 			return this.folder.name;
 		}
@@ -47,6 +50,39 @@ export default Vue.extend({
 			this.browser.move(this.folder);
 		},
 
+		onContextmenu(e) {
+			this.isContextmenuShowing = true;
+			contextmenu(e, [{
+				type: 'item',
+				text: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.move-to-this-folder%',
+				icon: '%fa:arrow-right%',
+				onClick: this.go
+			}, {
+				type: 'item',
+				text: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.show-in-new-window%',
+				icon: '%fa:R window-restore%',
+				onClick: this.newWindow
+			}, {
+				type: 'divider',
+			}, {
+				type: 'item',
+				text: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.rename%',
+				icon: '%fa:i-cursor%',
+				onClick: this.rename
+			}, {
+				type: 'divider',
+			}, {
+				type: 'item',
+				text: '%i18n:common.delete%',
+				icon: '%fa:R trash-alt%',
+				onClick: this.deleteFolder
+			}], {
+				closed: () => {
+					this.isContextmenuShowing = false;
+				}
+			});
+		},
+
 		onMouseover() {
 			this.hover = true;
 		},
@@ -102,7 +138,7 @@ export default Vue.extend({
 			if (obj.type == 'file') {
 				const file = obj.id;
 				this.browser.removeFile(file);
-				this.$root.$data.os.api('drive/files/update', {
+				(this as any).api('drive/files/update', {
 					file_id: file,
 					folder_id: this.folder.id
 				});
@@ -112,7 +148,7 @@ export default Vue.extend({
 				// 移動先が自分自身ならreject
 				if (folder == this.folder.id) return false;
 				this.browser.removeFolder(folder);
-				this.$root.$data.os.api('drive/folders/update', {
+				(this as any).api('drive/folders/update', {
 					folder_id: folder,
 					parent_id: this.folder.id
 				}).then(() => {
@@ -120,10 +156,13 @@ export default Vue.extend({
 				}).catch(err => {
 					switch (err) {
 						case 'detected-circular-definition':
-							dialog('%fa:exclamation-triangle%%i18n:desktop.tags.mk-drive-browser-folder.unable-to-process%',
-								'%i18n:desktop.tags.mk-drive-browser-folder.circular-reference-detected%', [{
-								text: '%i18n:common.ok%'
-							}]);
+							(this as any).apis.dialog({
+								title: '%fa:exclamation-triangle%%i18n:desktop.tags.mk-drive-browser-folder.unable-to-process%',
+								text: '%i18n:desktop.tags.mk-drive-browser-folder.circular-reference-detected%',
+								actions: [{
+									text: '%i18n:common.ok%'
+								}]
+							});
 							break;
 						default:
 							alert('%i18n:desktop.tags.mk-drive-browser-folder.unhandled-error% ' + err);
@@ -152,21 +191,29 @@ export default Vue.extend({
 			this.browser.isDragSource = false;
 		},
 
-		onContextmenu(e) {
-			this.isContextmenuShowing = true;
-			const ctx = new MkDriveFolderContextmenu({
-				parent: this,
-				propsData: {
-					browser: this.browser,
-					x: e.pageX - window.pageXOffset,
-					y: e.pageY - window.pageYOffset
-				}
-			}).$mount();
-			ctx.$once('closed', () => {
-				this.isContextmenuShowing = false;
+		go() {
+			this.browser.move(this.folder.id);
+		},
+
+		newWindow() {
+			this.browser.newWindow(this.folder.id);
+		},
+
+		rename() {
+			(this as any).apis.input({
+				title: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.rename-folder%',
+				placeholder: '%i18n:desktop.tags.mk-drive-browser-folder-contextmenu.input-new-folder-name%',
+				default: this.folder.name
+			}).then(name => {
+				(this as any).api('drive/folders/update', {
+					folder_id: this.folder.id,
+					name: name
+				});
 			});
-			document.body.appendChild(ctx.$el);
-			return false;
+		},
+
+		deleteFolder() {
+			alert('not implemented yet');
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/drive-nav-folder.vue b/src/web/app/desktop/views/components/drive-nav-folder.vue
index 556c64f11..b6eb36535 100644
--- a/src/web/app/desktop/views/components/drive-nav-folder.vue
+++ b/src/web/app/desktop/views/components/drive-nav-folder.vue
@@ -73,7 +73,7 @@ export default Vue.extend({
 			if (obj.type == 'file') {
 				const file = obj.id;
 				this.browser.removeFile(file);
-				this.$root.$data.os.api('drive/files/update', {
+				(this as any).api('drive/files/update', {
 					file_id: file,
 					folder_id: this.folder ? this.folder.id : null
 				});
@@ -83,7 +83,7 @@ export default Vue.extend({
 				// 移動先が自分自身ならreject
 				if (this.folder && folder == this.folder.id) return false;
 				this.browser.removeFolder(folder);
-				this.$root.$data.os.api('drive/folders/update', {
+				(this as any).api('drive/folders/update', {
 					folder_id: folder,
 					parent_id: this.folder ? this.folder.id : null
 				});
diff --git a/src/web/app/desktop/views/components/drive-window.vue b/src/web/app/desktop/views/components/drive-window.vue
new file mode 100644
index 000000000..0f0d8d81b
--- /dev/null
+++ b/src/web/app/desktop/views/components/drive-window.vue
@@ -0,0 +1,53 @@
+<template>
+<mk-window ref="window" @closed="$destroy" width="800px" height="500px" :popout="popout">
+	<span slot="header" :class="$style.header">
+		<p class="info" v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:desktop.tags.mk-drive-browser-window.used%</p>
+		%fa:cloud%%i18n:desktop.tags.mk-drive-browser-window.drive%
+	</span>
+	<mk-drive-browser multiple :folder="folder" ref="browser"/>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { url } from '../../../config';
+
+export default Vue.extend({
+	props: ['folder'],
+	data() {
+		return {
+			usage: null
+		};
+	},
+	mounted() {
+		(this as any).api('drive').then(info => {
+			this.usage = info.usage / info.capacity * 100;
+		});
+	},
+	methods: {
+		popout() {
+			const folder = (this.$refs.browser as any).folder;
+			if (folder) {
+				return `${url}/i/drive/folder/${folder.id}`;
+			} else {
+				return `${url}/i/drive`;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.header
+	> [data-fa]
+		margin-right 4px
+
+.info
+	position absolute
+	top 0
+	left 16px
+	margin 0
+	font-size 80%
+
+</style>
+
diff --git a/src/web/app/desktop/views/components/drive.vue b/src/web/app/desktop/views/components/drive.vue
index 5d398dab9..2b33265e5 100644
--- a/src/web/app/desktop/views/components/drive.vue
+++ b/src/web/app/desktop/views/components/drive.vue
@@ -2,17 +2,17 @@
 <div class="mk-drive">
 	<nav>
 		<div class="path" @contextmenu.prevent.stop="() => {}">
-			<mk-drive-browser-nav-folder :class="{ current: folder == null }" folder={ null }/>
-			<template each={ folder in hierarchyFolders }>
-				<span class="separator">%fa:angle-right%</span>
-				<mk-drive-browser-nav-folder folder={ folder }/>
+			<mk-drive-nav-folder :class="{ current: folder == null }"/>
+			<template v-for="folder in hierarchyFolders">
+				<span class="separator" :key="folder.id + '>'">%fa:angle-right%</span>
+				<mk-drive-nav-folder :folder="folder" :key="folder.id"/>
 			</template>
 			<span class="separator" v-if="folder != null">%fa:angle-right%</span>
-			<span class="folder current" v-if="folder != null">{ folder.name }</span>
+			<span class="folder current" v-if="folder != null">{{ folder.name }}</span>
 		</div>
 		<input class="search" type="search" placeholder="&#xf002; %i18n:desktop.tags.mk-drive-browser.search%"/>
 	</nav>
-	<div class="main { uploading: uploads.length > 0, fetching: fetching }"
+	<div class="main" :class="{ uploading: uploadings.length > 0, fetching }"
 		ref="main"
 		@mousedown="onMousedown"
 		@dragover.prevent.stop="onDragover"
@@ -24,19 +24,15 @@
 		<div class="selection" ref="selection"></div>
 		<div class="contents" ref="contents">
 			<div class="folders" ref="foldersContainer" v-if="folders.length > 0">
-				<template each={ folder in folders }>
-					<mk-drive-browser-folder class="folder" folder={ folder }/>
-				</template>
+				<mk-drive-folder v-for="folder in folders" :key="folder.id" class="folder" :folder="folder"/>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
-				<div class="padding" each={ Array(10).fill(16) }></div>
+				<div class="padding" v-for="n in 16" :key="n"></div>
 				<button v-if="moreFolders">%i18n:desktop.tags.mk-drive-browser.load-more%</button>
 			</div>
 			<div class="files" ref="filesContainer" v-if="files.length > 0">
-				<template each={ file in files }>
-					<mk-drive-browser-file class="file" file={ file }/>
-				</template>
+				<mk-drive-file v-for="file in files" :key="file.id" class="file" :file="file"/>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
-				<div class="padding" each={ Array(10).fill(16) }></div>
+				<div class="padding" v-for="n in 16" :key="n"></div>
 				<button v-if="moreFiles" @click="fetchMoreFiles">%i18n:desktop.tags.mk-drive-browser.load-more%</button>
 			</div>
 			<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
@@ -60,16 +56,18 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import MkDriveWindow from './drive-window.vue';
 import contains from '../../../common/scripts/contains';
-import dialog from '../../scripts/dialog';
-import inputDialog from '../../scripts/input-dialog';
+import contextmenu from '../../api/contextmenu';
 
 export default Vue.extend({
 	props: {
 		initFolder: {
+			type: Object,
 			required: false
 		},
 		multiple: {
+			type: Boolean,
 			default: false
 		}
 	},
@@ -106,8 +104,8 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.connection = this.$root.$data.os.streams.driveStream.getConnection();
-		this.connectionId = this.$root.$data.os.streams.driveStream.use();
+		this.connection = (this as any).os.streams.driveStream.getConnection();
+		this.connectionId = (this as any).os.streams.driveStream.use();
 
 		this.connection.on('file_created', this.onStreamDriveFileCreated);
 		this.connection.on('file_updated', this.onStreamDriveFileUpdated);
@@ -125,12 +123,32 @@ export default Vue.extend({
 		this.connection.off('file_updated', this.onStreamDriveFileUpdated);
 		this.connection.off('folder_created', this.onStreamDriveFolderCreated);
 		this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
-		this.$root.$data.os.streams.driveStream.dispose(this.connectionId);
+		(this as any).os.streams.driveStream.dispose(this.connectionId);
 	},
 	methods: {
+		onContextmenu(e) {
+			contextmenu(e, [{
+				type: 'item',
+				text: '%i18n:desktop.tags.mk-drive-browser-base-contextmenu.create-folder%',
+				icon: '%fa:R folder%',
+				onClick: this.createFolder
+			}, {
+				type: 'item',
+				text: '%i18n:desktop.tags.mk-drive-browser-base-contextmenu.upload%',
+				icon: '%fa:upload%',
+				onClick: this.selectLocalFile
+			}, {
+				type: 'item',
+				text: '%i18n:desktop.tags.mk-drive-browser-base-contextmenu.url-upload%',
+				icon: '%fa:cloud-upload-alt%',
+				onClick: this.urlUpload
+			}]);
+		},
+
 		onStreamDriveFileCreated(file) {
 			this.addFile(file, true);
 		},
+
 		onStreamDriveFileUpdated(file) {
 			const current = this.folder ? this.folder.id : null;
 			if (current != file.folder_id) {
@@ -139,9 +157,11 @@ export default Vue.extend({
 				this.addFile(file, true);
 			}
 		},
+
 		onStreamDriveFolderCreated(folder) {
 			this.addFolder(folder, true);
 		},
+
 		onStreamDriveFolderUpdated(folder) {
 			const current = this.folder ? this.folder.id : null;
 			if (current != folder.parent_id) {
@@ -150,12 +170,15 @@ export default Vue.extend({
 				this.addFolder(folder, true);
 			}
 		},
+
 		onChangeUploaderUploads(uploads) {
 			this.uploadings = uploads;
 		},
+
 		onUploaderUploaded(file) {
 			this.addFile(file, true);
 		},
+
 		onMousedown(e): any {
 			if (contains(this.$refs.foldersContainer, e.target) || contains(this.$refs.filesContainer, e.target)) return true;
 
@@ -202,6 +225,7 @@ export default Vue.extend({
 			document.documentElement.addEventListener('mousemove', move);
 			document.documentElement.addEventListener('mouseup', up);
 		},
+
 		onDragover(e): any {
 			// ドラッグ元が自分自身の所有するアイテムかどうか
 			if (!this.isDragSource) {
@@ -214,12 +238,15 @@ export default Vue.extend({
 				return false;
 			}
 		},
+
 		onDragenter(e) {
 			if (!this.isDragSource) this.draghover = true;
 		},
+
 		onDragleave(e) {
 			this.draghover = false;
 		},
+
 		onDrop(e): any {
 			this.draghover = false;
 
@@ -244,7 +271,7 @@ export default Vue.extend({
 				const file = obj.id;
 				if (this.files.some(f => f.id == file)) return false;
 				this.removeFile(file);
-				this.$root.$data.os.api('drive/files/update', {
+				(this as any).api('drive/files/update', {
 					file_id: file,
 					folder_id: this.folder ? this.folder.id : null
 				});
@@ -255,7 +282,7 @@ export default Vue.extend({
 				if (this.folder && folder == this.folder.id) return false;
 				if (this.folders.some(f => f.id == folder)) return false;
 				this.removeFolder(folder);
-				this.$root.$data.os.api('drive/folders/update', {
+				(this as any).api('drive/folders/update', {
 					folder_id: folder,
 					parent_id: this.folder ? this.folder.id : null
 				}).then(() => {
@@ -263,10 +290,13 @@ export default Vue.extend({
 				}).catch(err => {
 					switch (err) {
 						case 'detected-circular-definition':
-							dialog('%fa:exclamation-triangle%%i18n:desktop.tags.mk-drive-browser.unable-to-process%',
-								'%i18n:desktop.tags.mk-drive-browser.circular-reference-detected%', [{
-								text: '%i18n:common.ok%'
-							}]);
+							(this as any).apis.dialog({
+								title: '%fa:exclamation-triangle%%i18n:desktop.tags.mk-drive-browser.unable-to-process%',
+								text: '%i18n:desktop.tags.mk-drive-browser.circular-reference-detected%',
+								actions: [{
+									text: '%i18n:common.ok%'
+								}]
+							});
 							break;
 						default:
 							alert('%i18n:desktop.tags.mk-drive-browser.unhandled-error% ' + err);
@@ -276,40 +306,37 @@ export default Vue.extend({
 
 			return false;
 		},
-		onContextmenu(e) {
-			document.body.appendChild(new MkDriveContextmenu({
-				propsData: {
-					browser: this,
-					x: e.pageX - window.pageXOffset,
-					y: e.pageY - window.pageYOffset
-				}
-			}).$mount().$el);
 
-			return false;
-		},
 		selectLocalFile() {
 			(this.$refs.fileInput as any).click();
 		},
-		urlUpload() {
-			inputDialog('%i18n:desktop.tags.mk-drive-browser.url-upload%',
-				'%i18n:desktop.tags.mk-drive-browser.url-of-file%', null, url => {
 
-				this.$root.$data.os.api('drive/files/upload_from_url', {
+		urlUpload() {
+			(this as any).apis.input({
+				title: '%i18n:desktop.tags.mk-drive-browser.url-upload%',
+				placeholder: '%i18n:desktop.tags.mk-drive-browser.url-of-file%'
+			}).then(url => {
+				(this as any).api('drive/files/upload_from_url', {
 					url: url,
 					folder_id: this.folder ? this.folder.id : undefined
 				});
 
-				dialog('%fa:check%%i18n:desktop.tags.mk-drive-browser.url-upload-requested%',
-					'%i18n:desktop.tags.mk-drive-browser.may-take-time%', [{
-					text: '%i18n:common.ok%'
-				}]);
+				(this as any).apis.dialog({
+					title: '%fa:check%%i18n:desktop.tags.mk-drive-browser.url-upload-requested%',
+					text: '%i18n:desktop.tags.mk-drive-browser.may-take-time%',
+					actions: [{
+						text: '%i18n:common.ok%'
+					}]
+				});
 			});
 		},
-		createFolder() {
-			inputDialog('%i18n:desktop.tags.mk-drive-browser.create-folder%',
-				'%i18n:desktop.tags.mk-drive-browser.folder-name%', null, name => {
 
-				this.$root.$data.os.api('drive/folders/create', {
+		createFolder() {
+			(this as any).apis.input({
+				title: '%i18n:desktop.tags.mk-drive-browser.create-folder%',
+				placeholder: '%i18n:desktop.tags.mk-drive-browser.folder-name%'
+			}).then(name => {
+				(this as any).api('drive/folders/create', {
 					name: name,
 					folder_id: this.folder ? this.folder.id : undefined
 				}).then(folder => {
@@ -317,15 +344,18 @@ export default Vue.extend({
 				});
 			});
 		},
+
 		onChangeFileInput() {
 			Array.from((this.$refs.fileInput as any).files).forEach(file => {
 				this.upload(file, this.folder);
 			});
 		},
+
 		upload(file, folder) {
 			if (folder && typeof folder == 'object') folder = folder.id;
 			(this.$refs.uploader as any).upload(file, folder);
 		},
+
 		chooseFile(file) {
 			const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id);
 			if (this.multiple) {
@@ -344,6 +374,7 @@ export default Vue.extend({
 				}
 			}
 		},
+
 		newWindow(folderId) {
 			document.body.appendChild(new MkDriveWindow({
 				propsData: {
@@ -351,6 +382,7 @@ export default Vue.extend({
 				}
 			}).$mount().$el);
 		},
+
 		move(target) {
 			if (target == null) {
 				this.goRoot();
@@ -361,7 +393,7 @@ export default Vue.extend({
 
 			this.fetching = true;
 
-			this.$root.$data.os.api('drive/folders/show', {
+			(this as any).api('drive/folders/show', {
 				folder_id: target
 			}).then(folder => {
 				this.folder = folder;
@@ -378,6 +410,7 @@ export default Vue.extend({
 				this.fetch();
 			});
 		},
+
 		addFolder(folder, unshift = false) {
 			const current = this.folder ? this.folder.id : null;
 			if (current != folder.parent_id) return;
@@ -394,6 +427,7 @@ export default Vue.extend({
 				this.folders.push(folder);
 			}
 		},
+
 		addFile(file, unshift = false) {
 			const current = this.folder ? this.folder.id : null;
 			if (current != file.folder_id) return;
@@ -410,26 +444,33 @@ export default Vue.extend({
 				this.files.push(file);
 			}
 		},
+
 		removeFolder(folder) {
 			if (typeof folder == 'object') folder = folder.id;
 			this.folders = this.folders.filter(f => f.id != folder);
 		},
+
 		removeFile(file) {
 			if (typeof file == 'object') file = file.id;
 			this.files = this.files.filter(f => f.id != file);
 		},
+
 		appendFile(file) {
 			this.addFile(file);
 		},
+
 		appendFolder(folder) {
 			this.addFolder(folder);
 		},
+
 		prependFile(file) {
 			this.addFile(file, true);
 		},
+
 		prependFolder(folder) {
 			this.addFolder(folder, true);
 		},
+
 		goRoot() {
 			// 既にrootにいるなら何もしない
 			if (this.folder == null) return;
@@ -439,6 +480,7 @@ export default Vue.extend({
 			this.$emit('move-root');
 			this.fetch();
 		},
+
 		fetch() {
 			this.folders = [];
 			this.files = [];
@@ -453,7 +495,7 @@ export default Vue.extend({
 			const filesMax = 30;
 
 			// フォルダ一覧取得
-			this.$root.$data.os.api('drive/folders', {
+			(this as any).api('drive/folders', {
 				folder_id: this.folder ? this.folder.id : null,
 				limit: foldersMax + 1
 			}).then(folders => {
@@ -466,7 +508,7 @@ export default Vue.extend({
 			});
 
 			// ファイル一覧取得
-			this.$root.$data.os.api('drive/files', {
+			(this as any).api('drive/files', {
 				folder_id: this.folder ? this.folder.id : null,
 				limit: filesMax + 1
 			}).then(files => {
@@ -489,13 +531,14 @@ export default Vue.extend({
 				}
 			};
 		},
+
 		fetchMoreFiles() {
 			this.fetching = true;
 
 			const max = 30;
 
 			// ファイル一覧取得
-			this.$root.$data.os.api('drive/files', {
+			(this as any).api('drive/files', {
 				folder_id: this.folder ? this.folder.id : null,
 				limit: max + 1
 			}).then(files => {
diff --git a/src/web/app/desktop/views/components/follow-button.vue b/src/web/app/desktop/views/components/follow-button.vue
index 0fffbda91..c4c3063ae 100644
--- a/src/web/app/desktop/views/components/follow-button.vue
+++ b/src/web/app/desktop/views/components/follow-button.vue
@@ -29,8 +29,8 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.connection = this.$root.$data.os.stream.getConnection();
-		this.connectionId = this.$root.$data.os.stream.use();
+		this.connection = (this as any).os.stream.getConnection();
+		this.connectionId = (this as any).os.stream.use();
 
 		this.connection.on('follow', this.onFollow);
 		this.connection.on('unfollow', this.onUnfollow);
@@ -38,7 +38,7 @@ export default Vue.extend({
 	beforeDestroy() {
 		this.connection.off('follow', this.onFollow);
 		this.connection.off('unfollow', this.onUnfollow);
-		this.$root.$data.os.stream.dispose(this.connectionId);
+		(this as any).os.stream.dispose(this.connectionId);
 	},
 	methods: {
 
@@ -57,7 +57,7 @@ export default Vue.extend({
 		onClick() {
 			this.wait = true;
 			if (this.user.is_following) {
-				this.$root.$data.os.api('following/delete', {
+				(this as any).api('following/delete', {
 					user_id: this.user.id
 				}).then(() => {
 					this.user.is_following = false;
@@ -67,7 +67,7 @@ export default Vue.extend({
 					this.wait = false;
 				});
 			} else {
-				this.$root.$data.os.api('following/create', {
+				(this as any).api('following/create', {
 					user_id: this.user.id
 				}).then(() => {
 					this.user.is_following = true;
diff --git a/src/web/app/desktop/views/components/friends-maker.vue b/src/web/app/desktop/views/components/friends-maker.vue
index caa5f4913..b23373421 100644
--- a/src/web/app/desktop/views/components/friends-maker.vue
+++ b/src/web/app/desktop/views/components/friends-maker.vue
@@ -39,7 +39,7 @@ export default Vue.extend({
 			this.fetching = true;
 			this.users = [];
 
-			this.$root.$data.os.api('users/recommendation', {
+			(this as any).api('users/recommendation', {
 				limit: this.limit,
 				offset: this.limit * this.page
 			}).then(users => {
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 076cbabe8..f5f33e587 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -101,7 +101,7 @@ export default Vue.extend({
 	},
 	methods: {
 		bakeHomeData() {
-			return JSON.stringify(this.$root.$data.os.i.client_settings.home);
+			return JSON.stringify((this as any).os.i.client_settings.home);
 		},
 		onTlLoaded() {
 			this.$emit('loaded');
@@ -123,7 +123,7 @@ export default Vue.extend({
 				data: {}
 			};
 
-			this.$root.$data.os.i.client_settings.home.unshift(widget);
+			(this as any).os.i.client_settings.home.unshift(widget);
 
 			this.saveHome();
 		},
@@ -132,48 +132,48 @@ export default Vue.extend({
 
 			Array.from((this.$refs.left as Element).children).forEach(el => {
 				const id = el.getAttribute('data-widget-id');
-				const widget = this.$root.$data.os.i.client_settings.home.find(w => w.id == id);
+				const widget = (this as any).os.i.client_settings.home.find(w => w.id == id);
 				widget.place = 'left';
 				data.push(widget);
 			});
 
 			Array.from((this.$refs.right as Element).children).forEach(el => {
 				const id = el.getAttribute('data-widget-id');
-				const widget = this.$root.$data.os.i.client_settings.home.find(w => w.id == id);
+				const widget = (this as any).os.i.client_settings.home.find(w => w.id == id);
 				widget.place = 'right';
 				data.push(widget);
 			});
 
 			Array.from((this.$refs.maintop as Element).children).forEach(el => {
 				const id = el.getAttribute('data-widget-id');
-				const widget = this.$root.$data.os.i.client_settings.home.find(w => w.id == id);
+				const widget = (this as any).os.i.client_settings.home.find(w => w.id == id);
 				widget.place = 'main';
 				data.push(widget);
 			});
 
-			this.$root.$data.os.api('i/update_home', {
+			(this as any).api('i/update_home', {
 				home: data
 			});
 		}
 	},
 	computed: {
 		leftWidgets(): any {
-			return this.$root.$data.os.i.client_settings.home.filter(w => w.place == 'left');
+			return (this as any).os.i.client_settings.home.filter(w => w.place == 'left');
 		},
 		centerWidgets(): any {
-			return this.$root.$data.os.i.client_settings.home.filter(w => w.place == 'center');
+			return (this as any).os.i.client_settings.home.filter(w => w.place == 'center');
 		},
 		rightWidgets(): any {
-			return this.$root.$data.os.i.client_settings.home.filter(w => w.place == 'right');
+			return (this as any).os.i.client_settings.home.filter(w => w.place == 'right');
 		}
 	},
 	created() {
 		this.bakedHomeData = this.bakeHomeData();
 	},
 	mounted() {
-		this.$root.$data.os.i.on('refreshed', this.onMeRefreshed);
+		(this as any).os.i.on('refreshed', this.onMeRefreshed);
 
-		this.home = this.$root.$data.os.i.client_settings.home;
+		this.home = (this as any).os.i.client_settings.home;
 
 		if (!this.customize) {
 			if ((this.$refs.left as Element).children.length == 0) {
@@ -214,14 +214,14 @@ export default Vue.extend({
 					const el = evt.item;
 					const id = el.getAttribute('data-widget-id');
 					el.parentNode.removeChild(el);
-					this.$root.$data.os.i.client_settings.home = this.$root.$data.os.i.client_settings.home.filter(w => w.id != id);
+					(this as any).os.i.client_settings.home = (this as any).os.i.client_settings.home.filter(w => w.id != id);
 					this.saveHome();
 				}
 			}));
 		}
 	},
 	beforeDestroy() {
-		this.$root.$data.os.i.off('refreshed', this.onMeRefreshed);
+		(this as any).os.i.off('refreshed', this.onMeRefreshed);
 
 		this.home.forEach(widget => {
 			widget.unmount();
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 1e4c2bafc..1e4bd96a1 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -27,6 +27,10 @@ import postForm from './post-form.vue';
 import repostForm from './repost-form.vue';
 import followButton from './follow-button.vue';
 import postPreview from './post-preview.vue';
+import drive from './drive.vue';
+import driveFile from './drive-file.vue';
+import driveFolder from './drive-folder.vue';
+import driveNavFolder from './drive-nav-folder.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-header', uiHeader);
@@ -55,3 +59,7 @@ Vue.component('mk-post-form', postForm);
 Vue.component('mk-repost-form', repostForm);
 Vue.component('mk-follow-button', followButton);
 Vue.component('mk-post-preview', postPreview);
+Vue.component('mk-drive', drive);
+Vue.component('mk-drive-file', driveFile);
+Vue.component('mk-drive-folder', driveFolder);
+Vue.component('mk-drive-nav-folder', driveNavFolder);
diff --git a/src/web/app/desktop/views/components/input-dialog.vue b/src/web/app/desktop/views/components/input-dialog.vue
index a78c7dcba..c00b1d4c1 100644
--- a/src/web/app/desktop/views/components/input-dialog.vue
+++ b/src/web/app/desktop/views/components/input-dialog.vue
@@ -33,12 +33,6 @@ export default Vue.extend({
 		},
 		type: {
 			default: 'text'
-		},
-		onOk: {
-			type: Function
-		},
-		onCancel: {
-			type: Function
 		}
 	},
 	data() {
@@ -63,9 +57,9 @@ export default Vue.extend({
 		},
 		beforeClose() {
 			if (this.done) {
-				this.onOk(this.text);
+				this.$emit('done', this.text);
 			} else {
-				if (this.onCancel) this.onCancel();
+				this.$emit('canceled');
 			}
 		},
 		onKeydown(e) {
diff --git a/src/web/app/desktop/views/components/messaging-window.vue b/src/web/app/desktop/views/components/messaging-window.vue
index f8df20bc1..0dbcddbec 100644
--- a/src/web/app/desktop/views/components/messaging-window.vue
+++ b/src/web/app/desktop/views/components/messaging-window.vue
@@ -11,7 +11,6 @@ export default Vue.extend({
 	methods: {
 		navigate(user) {
 			document.body.appendChild(new MkMessagingRoomWindow({
-				parent: this,
 				propsData: {
 					user: user
 				}
diff --git a/src/web/app/desktop/views/components/mute-setting.vue b/src/web/app/desktop/views/components/mute-setting.vue
index a8813172a..3fcc34c9e 100644
--- a/src/web/app/desktop/views/components/mute-setting.vue
+++ b/src/web/app/desktop/views/components/mute-setting.vue
@@ -22,7 +22,7 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.$root.$data.os.api('mute/list').then(x => {
+		(this as any).api('mute/list').then(x => {
 			this.fetching = false;
 			this.users = x.users;
 		});
diff --git a/src/web/app/desktop/views/components/notifications.vue b/src/web/app/desktop/views/components/notifications.vue
index f19766dc8..443ebea2a 100644
--- a/src/web/app/desktop/views/components/notifications.vue
+++ b/src/web/app/desktop/views/components/notifications.vue
@@ -128,14 +128,14 @@ export default Vue.extend({
 		}
 	},
 	mounted() {
-		this.connection = this.$root.$data.os.stream.getConnection();
-		this.connectionId = this.$root.$data.os.stream.use();
+		this.connection = (this as any).os.stream.getConnection();
+		this.connectionId = (this as any).os.stream.use();
 
 		this.connection.on('notification', this.onNotification);
 
 		const max = 10;
 
-		this.$root.$data.os.api('i/notifications', {
+		(this as any).api('i/notifications', {
 			limit: max + 1
 		}).then(notifications => {
 			if (notifications.length == max + 1) {
@@ -149,7 +149,7 @@ export default Vue.extend({
 	},
 	beforeDestroy() {
 		this.connection.off('notification', this.onNotification);
-		this.$root.$data.os.stream.dispose(this.connectionId);
+		(this as any).os.stream.dispose(this.connectionId);
 	},
 	methods: {
 		fetchMoreNotifications() {
@@ -157,7 +157,7 @@ export default Vue.extend({
 
 			const max = 30;
 
-			this.$root.$data.os.api('i/notifications', {
+			(this as any).api('i/notifications', {
 				limit: max + 1,
 				until_id: this.notifications[this.notifications.length - 1].id
 			}).then(notifications => {
diff --git a/src/web/app/desktop/views/components/password-setting.vue b/src/web/app/desktop/views/components/password-setting.vue
index 2e3e4fb6f..883a494cc 100644
--- a/src/web/app/desktop/views/components/password-setting.vue
+++ b/src/web/app/desktop/views/components/password-setting.vue
@@ -22,7 +22,7 @@ export default Vue.extend({
 							}]);
 							return;
 						}
-						this.$root.$data.os.api('i/change_password', {
+						(this as any).api('i/change_password', {
 							current_password: currentPassword,
 							new_password: newPassword
 						}).then(() => {
diff --git a/src/web/app/desktop/views/components/post-detail-sub.vue b/src/web/app/desktop/views/components/post-detail-sub.vue
index 8d81e6860..44ed5edd8 100644
--- a/src/web/app/desktop/views/components/post-detail-sub.vue
+++ b/src/web/app/desktop/views/components/post-detail-sub.vue
@@ -16,7 +16,7 @@
 			</div>
 		</header>
 		<div class="body">
-			<mk-post-html v-if="post.ast" :ast="post.ast" :i="$root.$data.os.i"/>
+			<mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i"/>
 			<div class="media" v-if="post.media">
 				<mk-images images={ post.media }/>
 			</div>
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index d23043dd4..dd4a32b6e 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -35,7 +35,7 @@
 			</a>
 		</header>
 		<div class="body">
-			<mk-post-html v-if="p.ast" :ast="p.ast" :i="$root.$data.os.i"/>
+			<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i"/>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			<div class="media" v-if="p.media">
 				<mk-images images={ p.media }/>
@@ -117,7 +117,7 @@ export default Vue.extend({
 	mounted() {
 		// Get replies
 		if (!this.compact) {
-			this.$root.$data.os.api('posts/replies', {
+			(this as any).api('posts/replies', {
 				post_id: this.p.id,
 				limit: 8
 			}).then(replies => {
@@ -130,7 +130,7 @@ export default Vue.extend({
 			this.contextFetching = true;
 
 			// Fetch context
-			this.$root.$data.os.api('posts/context', {
+			(this as any).api('posts/context', {
 				post_id: this.p.reply_id
 			}).then(context => {
 				this.contextFetching = false;
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 502851316..456f0de82 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -113,18 +113,12 @@ export default Vue.extend({
 		chooseFile() {
 			(this.$refs.file as any).click();
 		},
-		chooseFileFromDrive() {/*
-			const w = new MkDriveFileSelectorWindow({
-				propsData: {
-					multiple: true
-				}
-			}).$mount();
-
-			document.body.appendChild(w.$el);
-
-			w.$once('selected', files => {
+		chooseFileFromDrive() {
+			(this as any).apis.chooseDriveFile({
+				multiple: true
+			}).then(files => {
 				files.forEach(this.attachMedia);
-			});*/
+			});
 		},
 		attachMedia(driveFile) {
 			this.files.push(driveFile);
@@ -196,7 +190,7 @@ export default Vue.extend({
 		post() {
 			this.posting = true;
 
-			this.$root.$data.os.api('posts/create', {
+			(this as any).api('posts/create', {
 				text: this.text == '' ? undefined : this.text,
 				media_ids: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
 				reply_id: this.reply ? this.reply.id : undefined,
diff --git a/src/web/app/desktop/views/components/posts-post.vue b/src/web/app/desktop/views/components/posts-post.vue
index e611b2513..90db8088c 100644
--- a/src/web/app/desktop/views/components/posts-post.vue
+++ b/src/web/app/desktop/views/components/posts-post.vue
@@ -32,7 +32,7 @@
 				<div class="text" ref="text">
 					<p class="channel" v-if="p.channel"><a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:</p>
 					<a class="reply" v-if="p.reply">%fa:reply%</a>
-					<mk-post-html v-if="p.ast" :ast="p.ast" :i="$root.$data.os.i"/>
+					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i"/>
 					<a class="quote" v-if="p.repost">RP:</a>
 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 				</div>
@@ -133,24 +133,24 @@ export default Vue.extend({
 		}
 	},
 	created() {
-		this.connection = this.$root.$data.os.stream.getConnection();
-		this.connectionId = this.$root.$data.os.stream.use();
+		this.connection = (this as any).os.stream.getConnection();
+		this.connectionId = (this as any).os.stream.use();
 	},
 	mounted() {
 		this.capture(true);
 
-		if (this.$root.$data.os.isSignedIn) {
+		if ((this as any).os.isSignedIn) {
 			this.connection.on('_connected_', this.onStreamConnected);
 		}
 	},
 	beforeDestroy() {
 		this.decapture(true);
 		this.connection.off('_connected_', this.onStreamConnected);
-		this.$root.$data.os.stream.dispose(this.connectionId);
+		(this as any).os.stream.dispose(this.connectionId);
 	},
 	methods: {
 		capture(withHandler = false) {
-			if (this.$root.$data.os.isSignedIn) {
+			if ((this as any).os.isSignedIn) {
 				this.connection.send({
 					type: 'capture',
 					id: this.post.id
@@ -159,7 +159,7 @@ export default Vue.extend({
 			}
 		},
 		decapture(withHandler = false) {
-			if (this.$root.$data.os.isSignedIn) {
+			if ((this as any).os.isSignedIn) {
 				this.connection.send({
 					type: 'decapture',
 					id: this.post.id
@@ -178,7 +178,7 @@ export default Vue.extend({
 		},
 		reply() {
 			document.body.appendChild(new MkPostFormWindow({
-				parent: this,
+
 				propsData: {
 					reply: this.p
 				}
@@ -186,7 +186,7 @@ export default Vue.extend({
 		},
 		repost() {
 			document.body.appendChild(new MkRepostFormWindow({
-				parent: this,
+
 				propsData: {
 					post: this.p
 				}
@@ -194,7 +194,7 @@ export default Vue.extend({
 		},
 		react() {
 			document.body.appendChild(new MkReactionPicker({
-				parent: this,
+
 				propsData: {
 					source: this.$refs.reactButton,
 					post: this.p
@@ -203,7 +203,7 @@ export default Vue.extend({
 		},
 		menu() {
 			document.body.appendChild(new MkPostMenu({
-				parent: this,
+
 				propsData: {
 					source: this.$refs.menuButton,
 					post: this.p
diff --git a/src/web/app/desktop/views/components/profile-setting.vue b/src/web/app/desktop/views/components/profile-setting.vue
index abf80d316..403488ef1 100644
--- a/src/web/app/desktop/views/components/profile-setting.vue
+++ b/src/web/app/desktop/views/components/profile-setting.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-profile-setting">
 	<label class="avatar ui from group">
-		<p>%i18n:desktop.tags.mk-profile-setting.avatar%</p><img class="avatar" :src="`${$root.$data.os.i.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<p>%i18n:desktop.tags.mk-profile-setting.avatar%</p><img class="avatar" :src="`${os.i.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		<button class="ui" @click="updateAvatar">%i18n:desktop.tags.mk-profile-setting.choice-avatar%</button>
 	</label>
 	<label class="ui from group">
@@ -32,18 +32,18 @@ import notify from '../../scripts/notify';
 export default Vue.extend({
 	data() {
 		return {
-			name: this.$root.$data.os.i.name,
-			location: this.$root.$data.os.i.location,
-			description: this.$root.$data.os.i.description,
-			birthday: this.$root.$data.os.i.birthday,
+			name: (this as any).os.i.name,
+			location: (this as any).os.i.location,
+			description: (this as any).os.i.description,
+			birthday: (this as any).os.i.birthday,
 		};
 	},
 	methods: {
 		updateAvatar() {
-			updateAvatar(this.$root.$data.os.i);
+			updateAvatar((this as any).os.i);
 		},
 		save() {
-			this.$root.$data.os.api('i/update', {
+			(this as any).api('i/update', {
 				name: this.name,
 				location: this.location || null,
 				description: this.description || null,
diff --git a/src/web/app/desktop/views/components/repost-form.vue b/src/web/app/desktop/views/components/repost-form.vue
index 04b045ad4..d4a6186c4 100644
--- a/src/web/app/desktop/views/components/repost-form.vue
+++ b/src/web/app/desktop/views/components/repost-form.vue
@@ -29,7 +29,7 @@ export default Vue.extend({
 	methods: {
 		ok() {
 			this.wait = true;
-			this.$root.$data.os.api('posts/create', {
+			(this as any).api('posts/create', {
 				repost_id: this.post.id
 			}).then(data => {
 				this.$emit('posted');
diff --git a/src/web/app/desktop/views/components/sub-post-content.vue b/src/web/app/desktop/views/components/sub-post-content.vue
index e5264cefc..f048eb4f0 100644
--- a/src/web/app/desktop/views/components/sub-post-content.vue
+++ b/src/web/app/desktop/views/components/sub-post-content.vue
@@ -2,7 +2,7 @@
 <div class="mk-sub-post-content">
 	<div class="body">
 		<a class="reply" v-if="post.reply_id">%fa:reply%</a>
-		<mk-post-html :ast="post.ast" :i="$root.$data.os.i"/>
+		<mk-post-html :ast="post.ast" :i="os.i"/>
 		<a class="quote" v-if="post.repost_id" :href="`/post:${post.repost_id}`">RP: ...</a>
 		<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 	</div>
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index 63b36ff54..3d792436e 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -30,12 +30,12 @@ export default Vue.extend({
 	},
 	computed: {
 		alone(): boolean {
-			return this.$root.$data.os.i.following_count == 0;
+			return (this as any).os.i.following_count == 0;
 		}
 	},
 	mounted() {
-		this.connection = this.$root.$data.os.stream.getConnection();
-		this.connectionId = this.$root.$data.os.stream.use();
+		this.connection = (this as any).os.stream.getConnection();
+		this.connectionId = (this as any).os.stream.use();
 
 		this.connection.on('post', this.onPost);
 		this.connection.on('follow', this.onChangeFollowing);
@@ -50,7 +50,7 @@ export default Vue.extend({
 		this.connection.off('post', this.onPost);
 		this.connection.off('follow', this.onChangeFollowing);
 		this.connection.off('unfollow', this.onChangeFollowing);
-		this.$root.$data.os.stream.dispose(this.connectionId);
+		(this as any).os.stream.dispose(this.connectionId);
 
 		document.removeEventListener('keydown', this.onKeydown);
 		window.removeEventListener('scroll', this.onScroll);
@@ -59,7 +59,7 @@ export default Vue.extend({
 		fetch(cb?) {
 			this.fetching = true;
 
-			this.$root.$data.os.api('posts/timeline', {
+			(this as any).api('posts/timeline', {
 				until_date: this.date ? (this.date as any).getTime() : undefined
 			}).then(posts => {
 				this.fetching = false;
@@ -70,7 +70,7 @@ export default Vue.extend({
 		more() {
 			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
 			this.moreFetching = true;
-			this.$root.$data.os.api('posts/timeline', {
+			(this as any).api('posts/timeline', {
 				until_id: this.posts[this.posts.length - 1].id
 			}).then(posts => {
 				this.moreFetching = false;
diff --git a/src/web/app/desktop/views/components/ui-header-account.vue b/src/web/app/desktop/views/components/ui-header-account.vue
index 8dbd9e5e3..420fa6994 100644
--- a/src/web/app/desktop/views/components/ui-header-account.vue
+++ b/src/web/app/desktop/views/components/ui-header-account.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="mk-ui-header-account">
 	<button class="header" :data-active="isOpen" @click="toggle">
-		<span class="username">{{ $root.$data.os.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
-		<img class="avatar" :src="`${ $root.$data.os.i.avatar_url }?thumbnail&size=64`" alt="avatar"/>
+		<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.avatar_url }?thumbnail&size=64`" alt="avatar"/>
 	</button>
 	<div class="menu" v-if="isOpen">
 		<ul>
 			<li>
-				<a :href="`/${ $root.$data.os.i.username }`">%fa:user%%i18n:desktop.tags.mk-ui-header-account.profile%%fa:angle-right%</a>
+				<a :href="`/${ os.i.username }`">%fa:user%%i18n:desktop.tags.mk-ui-header-account.profile%%fa:angle-right%</a>
 			</li>
 			<li @click="drive">
 				<p>%fa:cloud%%i18n:desktop.tags.mk-ui-header-account.drive%%fa:angle-right%</p>
diff --git a/src/web/app/desktop/views/components/ui-header-nav.vue b/src/web/app/desktop/views/components/ui-header-nav.vue
index d0092ebd2..fe0c38778 100644
--- a/src/web/app/desktop/views/components/ui-header-nav.vue
+++ b/src/web/app/desktop/views/components/ui-header-nav.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-ui-header-nav">
 	<ul>
-		<template v-if="$root.$data.os.isSignedIn">
+		<template v-if="os.isSignedIn">
 			<li class="home" :class="{ active: page == 'home' }">
 				<a href="/">
 					%fa:home%
@@ -44,15 +44,15 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		if (this.$root.$data.os.isSignedIn) {
-			this.connection = this.$root.$data.os.stream.getConnection();
-			this.connectionId = this.$root.$data.os.stream.use();
+		if ((this as any).os.isSignedIn) {
+			this.connection = (this as any).os.stream.getConnection();
+			this.connectionId = (this as any).os.stream.use();
 
 			this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
 			this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
 
 			// Fetch count of unread messaging messages
-			this.$root.$data.os.api('messaging/unread').then(res => {
+			(this as any).api('messaging/unread').then(res => {
 				if (res.count > 0) {
 					this.hasUnreadMessagingMessages = true;
 				}
@@ -60,10 +60,10 @@ export default Vue.extend({
 		}
 	},
 	beforeDestroy() {
-		if (this.$root.$data.os.isSignedIn) {
+		if ((this as any).os.isSignedIn) {
 			this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
 			this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
-			this.$root.$data.os.stream.dispose(this.connectionId);
+			(this as any).os.stream.dispose(this.connectionId);
 		}
 	},
 	methods: {
diff --git a/src/web/app/desktop/views/components/ui-header-notifications.vue b/src/web/app/desktop/views/components/ui-header-notifications.vue
index 5ffa28c91..d4dc553c5 100644
--- a/src/web/app/desktop/views/components/ui-header-notifications.vue
+++ b/src/web/app/desktop/views/components/ui-header-notifications.vue
@@ -23,15 +23,15 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		if (this.$root.$data.os.isSignedIn) {
-			this.connection = this.$root.$data.os.stream.getConnection();
-			this.connectionId = this.$root.$data.os.stream.use();
+		if ((this as any).os.isSignedIn) {
+			this.connection = (this as any).os.stream.getConnection();
+			this.connectionId = (this as any).os.stream.use();
 
 			this.connection.on('read_all_notifications', this.onReadAllNotifications);
 			this.connection.on('unread_notification', this.onUnreadNotification);
 
 			// Fetch count of unread notifications
-			this.$root.$data.os.api('notifications/get_unread_count').then(res => {
+			(this as any).api('notifications/get_unread_count').then(res => {
 				if (res.count > 0) {
 					this.hasUnreadNotifications = true;
 				}
@@ -39,10 +39,10 @@ export default Vue.extend({
 		}
 	},
 	beforeDestroy() {
-		if (this.$root.$data.os.isSignedIn) {
+		if ((this as any).os.isSignedIn) {
 			this.connection.off('read_all_notifications', this.onReadAllNotifications);
 			this.connection.off('unread_notification', this.onUnreadNotification);
-			this.$root.$data.os.stream.dispose(this.connectionId);
+			(this as any).os.stream.dispose(this.connectionId);
 		}
 	},
 	methods: {
diff --git a/src/web/app/desktop/views/components/ui-header.vue b/src/web/app/desktop/views/components/ui-header.vue
index 0d9ecc4a5..6b89985ad 100644
--- a/src/web/app/desktop/views/components/ui-header.vue
+++ b/src/web/app/desktop/views/components/ui-header.vue
@@ -10,9 +10,9 @@
 				</div>
 				<div class="right">
 					<mk-ui-header-search/>
-					<mk-ui-header-account v-if="$root.$data.os.isSignedIn"/>
-					<mk-ui-header-notifications v-if="$root.$data.os.isSignedIn"/>
-					<mk-ui-header-post-button v-if="$root.$data.os.isSignedIn"/>
+					<mk-ui-header-account v-if="os.isSignedIn"/>
+					<mk-ui-header-notifications v-if="os.isSignedIn"/>
+					<mk-ui-header-post-button v-if="os.isSignedIn"/>
 					<mk-ui-header-clock/>
 				</div>
 			</div>
diff --git a/src/web/app/desktop/views/components/ui.vue b/src/web/app/desktop/views/components/ui.vue
index 76851a0f1..af39dff7a 100644
--- a/src/web/app/desktop/views/components/ui.vue
+++ b/src/web/app/desktop/views/components/ui.vue
@@ -4,7 +4,7 @@
 	<div class="content">
 		<slot></slot>
 	</div>
-	<mk-stream-indicator v-if="$root.$data.os.isSignedIn"/>
+	<mk-stream-indicator v-if="os.isSignedIn"/>
 </div>
 </template>
 
diff --git a/src/web/app/desktop/views/components/user-followers.vue b/src/web/app/desktop/views/components/user-followers.vue
index 67e694cf4..4541a0007 100644
--- a/src/web/app/desktop/views/components/user-followers.vue
+++ b/src/web/app/desktop/views/components/user-followers.vue
@@ -14,7 +14,7 @@ export default Vue.extend({
 	props: ['user'],
 	methods: {
 		fetch(iknow, limit, cursor, cb) {
-			this.$root.$data.os.api('users/followers', {
+			(this as any).api('users/followers', {
 				user_id: this.user.id,
 				iknow: iknow,
 				limit: limit,
diff --git a/src/web/app/desktop/views/components/user-following.vue b/src/web/app/desktop/views/components/user-following.vue
index 16cc3c42f..e0b9f1169 100644
--- a/src/web/app/desktop/views/components/user-following.vue
+++ b/src/web/app/desktop/views/components/user-following.vue
@@ -14,7 +14,7 @@ export default Vue.extend({
 	props: ['user'],
 	methods: {
 		fetch(iknow, limit, cursor, cb) {
-			this.$root.$data.os.api('users/following', {
+			(this as any).api('users/following', {
 				user_id: this.user.id,
 				iknow: iknow,
 				limit: limit,
diff --git a/src/web/app/desktop/views/components/user-preview.vue b/src/web/app/desktop/views/components/user-preview.vue
index 71b17503b..df2c7e897 100644
--- a/src/web/app/desktop/views/components/user-preview.vue
+++ b/src/web/app/desktop/views/components/user-preview.vue
@@ -21,7 +21,7 @@
 				<p>フォロワー</p><a>{{ u.followers_count }}</a>
 			</div>
 		</div>
-		<mk-follow-button v-if="$root.$data.os.isSignedIn && user.id != $root.$data.os.i.id" :user="u"/>
+		<mk-follow-button v-if="os.isSignedIn && user.id != os.i.id" :user="u"/>
 	</template>
 </div>
 </template>
@@ -49,7 +49,7 @@ export default Vue.extend({
 				this.open();
 			});
 		} else {
-			this.$root.$data.os.api('users/show', {
+			(this as any).api('users/show', {
 				user_id: this.user[0] == '@' ? undefined : this.user,
 				username: this.user[0] == '@' ? this.user.substr(1) : undefined
 			}).then(user => {
diff --git a/src/web/app/desktop/views/components/user-timeline.vue b/src/web/app/desktop/views/components/user-timeline.vue
index bab32fd24..fa5b32f22 100644
--- a/src/web/app/desktop/views/components/user-timeline.vue
+++ b/src/web/app/desktop/views/components/user-timeline.vue
@@ -60,7 +60,7 @@ export default Vue.extend({
 			}
 		},
 		fetch(cb?) {
-			this.$root.$data.os.api('users/posts', {
+			(this as any).api('users/posts', {
 				user_id: this.user.id,
 				until_date: this.date ? this.date.getTime() : undefined,
 				with_replies: this.mode == 'with-replies'
@@ -73,7 +73,7 @@ export default Vue.extend({
 		more() {
 			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
 			this.moreFetching = true;
-			this.$root.$data.os.api('users/posts', {
+			(this as any).api('users/posts', {
 				user_id: this.user.id,
 				with_replies: this.mode == 'with-replies',
 				until_id: this.posts[this.posts.length - 1].id
diff --git a/src/web/app/desktop/views/components/users-list.vue b/src/web/app/desktop/views/components/users-list.vue
index 268fac4ec..12abb372e 100644
--- a/src/web/app/desktop/views/components/users-list.vue
+++ b/src/web/app/desktop/views/components/users-list.vue
@@ -3,7 +3,7 @@
 	<nav>
 		<div>
 			<span :data-is-active="mode == 'all'" @click="mode = 'all'">すべて<span>{{ count }}</span></span>
-			<span v-if="$root.$data.os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span>
+			<span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span>
 		</div>
 	</nav>
 	<div class="users" v-if="!fetching && users.length != 0">
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 946590d68..08e28007a 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -80,7 +80,7 @@ export default Vue.extend({
 
 	created() {
 		// ウィンドウをウィンドウシステムに登録
-		this.$root.$data.os.windows.add(this);
+		(this as any).os.windows.add(this);
 	},
 
 	mounted() {
@@ -97,7 +97,7 @@ export default Vue.extend({
 
 	destroyed() {
 		// ウィンドウをウィンドウシステムから削除
-		this.$root.$data.os.windows.remove(this);
+		(this as any).os.windows.remove(this);
 
 		window.removeEventListener('resize', this.onBrowserResize);
 	},
@@ -191,7 +191,7 @@ export default Vue.extend({
 		top() {
 			let z = 0;
 
-			this.$root.$data.os.windows.getAll().forEach(w => {
+			(this as any).os.windows.getAll().forEach(w => {
 				if (w == this) return;
 				const m = w.$refs.main;
 				const mz = Number(document.defaultView.getComputedStyle(m, null).zIndex);
diff --git a/src/web/app/desktop/views/pages/home.vue b/src/web/app/desktop/views/pages/home.vue
index 7dc234ac0..e19b7fc8f 100644
--- a/src/web/app/desktop/views/pages/home.vue
+++ b/src/web/app/desktop/views/pages/home.vue
@@ -26,8 +26,8 @@ export default Vue.extend({
 	mounted() {
 		document.title = 'Misskey';
 
-		this.connection = this.$root.$data.os.stream.getConnection();
-		this.connectionId = this.$root.$data.os.stream.use();
+		this.connection = (this as any).os.stream.getConnection();
+		this.connectionId = (this as any).os.stream.use();
 
 		this.connection.on('post', this.onStreamPost);
 		document.addEventListener('visibilitychange', this.onVisibilitychange, false);
@@ -36,12 +36,12 @@ export default Vue.extend({
 	},
 	beforeDestroy() {
 		this.connection.off('post', this.onStreamPost);
-		this.$root.$data.os.stream.dispose(this.connectionId);
+		(this as any).os.stream.dispose(this.connectionId);
 		document.removeEventListener('visibilitychange', this.onVisibilitychange);
 	},
 	methods: {
 		onStreamPost(post) {
-			if (document.hidden && post.user_id != this.$root.$data.os.i.id) {
+			if (document.hidden && post.user_id != (this as any).os.i.id) {
 				this.unreadCount++;
 				document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
 			}
diff --git a/src/web/app/desktop/views/pages/index.vue b/src/web/app/desktop/views/pages/index.vue
index 6377b6a27..bd32c17b3 100644
--- a/src/web/app/desktop/views/pages/index.vue
+++ b/src/web/app/desktop/views/pages/index.vue
@@ -1,5 +1,5 @@
 <template>
-	<component v-bind:is="$root.$data.os.isSignedIn ? 'home' : 'welcome'"></component>
+	<component v-bind:is="os.isSignedIn ? 'home' : 'welcome'"></component>
 </template>
 
 <script lang="ts">
diff --git a/src/web/app/desktop/views/pages/messaging-room.vue b/src/web/app/desktop/views/pages/messaging-room.vue
index 86230cb54..3e4fb256a 100644
--- a/src/web/app/desktop/views/pages/messaging-room.vue
+++ b/src/web/app/desktop/views/pages/messaging-room.vue
@@ -21,7 +21,7 @@ export default Vue.extend({
 
 		document.documentElement.style.background = '#fff';
 
-		this.$root.$data.os.api('users/show', {
+		(this as any).api('users/show', {
 			username: this.username
 		}).then(user => {
 			this.fetching = false;
diff --git a/src/web/app/desktop/views/pages/post.vue b/src/web/app/desktop/views/pages/post.vue
index 471f5a5c6..186ee332f 100644
--- a/src/web/app/desktop/views/pages/post.vue
+++ b/src/web/app/desktop/views/pages/post.vue
@@ -23,7 +23,7 @@ export default Vue.extend({
 	mounted() {
 		Progress.start();
 
-		this.$root.$data.os.api('posts/show', {
+		(this as any).api('posts/show', {
 			post_id: this.postId
 		}).then(post => {
 			this.fetching = false;
diff --git a/src/web/app/desktop/views/pages/search.vue b/src/web/app/desktop/views/pages/search.vue
index d8147e0d6..828aac8fe 100644
--- a/src/web/app/desktop/views/pages/search.vue
+++ b/src/web/app/desktop/views/pages/search.vue
@@ -44,7 +44,7 @@ export default Vue.extend({
 		document.addEventListener('keydown', this.onDocumentKeydown);
 		window.addEventListener('scroll', this.onScroll);
 
-		this.$root.$data.os.api('posts/search', parse(this.query)).then(posts => {
+		(this as any).api('posts/search', parse(this.query)).then(posts => {
 			this.fetching = false;
 			this.posts = posts;
 		});
@@ -65,7 +65,7 @@ export default Vue.extend({
 			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
 			this.offset += limit;
 			this.moreFetching = true;
-			return this.$root.$data.os.api('posts/search', Object.assign({}, parse(this.query), {
+			return (this as any).api('posts/search', Object.assign({}, parse(this.query), {
 				limit: limit,
 				offset: this.offset
 			})).then(posts => {
diff --git a/src/web/app/desktop/views/pages/user/user-followers-you-know.vue b/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
index 419008175..246ff865d 100644
--- a/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
+++ b/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
@@ -22,7 +22,7 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.$root.$data.os.api('users/followers', {
+		(this as any).api('users/followers', {
 			user_id: this.user.id,
 			iknow: true,
 			limit: 16
diff --git a/src/web/app/desktop/views/pages/user/user-friends.vue b/src/web/app/desktop/views/pages/user/user-friends.vue
index 15fb7a96e..d6b20aa27 100644
--- a/src/web/app/desktop/views/pages/user/user-friends.vue
+++ b/src/web/app/desktop/views/pages/user/user-friends.vue
@@ -27,7 +27,7 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.$root.$data.os.api('users/get_frequently_replied_users', {
+		(this as any).api('users/get_frequently_replied_users', {
 			user_id: this.user.id,
 			limit: 4
 		}).then(docs => {
diff --git a/src/web/app/desktop/views/pages/user/user-header.vue b/src/web/app/desktop/views/pages/user/user-header.vue
index 07f206d24..b4a24459c 100644
--- a/src/web/app/desktop/views/pages/user/user-header.vue
+++ b/src/web/app/desktop/views/pages/user/user-header.vue
@@ -51,9 +51,9 @@ export default Vue.extend({
 		},
 
 		onBannerClick() {
-			if (!this.$root.$data.os.isSignedIn || this.$root.$data.os.i.id != this.user.id) return;
+			if (!(this as any).os.isSignedIn || (this as any).os.i.id != this.user.id) return;
 
-			updateBanner(this.$root.$data.os.i, i => {
+			updateBanner((this as any).os.i, i => {
 				this.user.banner_url = i.banner_url;
 			});
 		}
diff --git a/src/web/app/desktop/views/pages/user/user-home.vue b/src/web/app/desktop/views/pages/user/user-home.vue
index dc0a03dab..2e67b1ec3 100644
--- a/src/web/app/desktop/views/pages/user/user-home.vue
+++ b/src/web/app/desktop/views/pages/user/user-home.vue
@@ -4,7 +4,7 @@
 		<div ref="left">
 			<mk-user-profile :user="user"/>
 			<mk-user-photos :user="user"/>
-			<mk-user-followers-you-know v-if="$root.$data.os.isSignedIn && $root.$data.os.i.id != user.id" :user="user"/>
+			<mk-user-followers-you-know v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
 			<p>%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.last_used_at"/></b></p>
 		</div>
 	</div>
diff --git a/src/web/app/desktop/views/pages/user/user-photos.vue b/src/web/app/desktop/views/pages/user/user-photos.vue
index fc51b9789..789d9af85 100644
--- a/src/web/app/desktop/views/pages/user/user-photos.vue
+++ b/src/web/app/desktop/views/pages/user/user-photos.vue
@@ -23,7 +23,7 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.$root.$data.os.api('users/posts', {
+		(this as any).api('users/posts', {
 			user_id: this.user.id,
 			with_media: true,
 			limit: 9
diff --git a/src/web/app/desktop/views/pages/user/user-profile.vue b/src/web/app/desktop/views/pages/user/user-profile.vue
index 66385ab2e..d389e01c1 100644
--- a/src/web/app/desktop/views/pages/user/user-profile.vue
+++ b/src/web/app/desktop/views/pages/user/user-profile.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-user-profile">
-	<div class="friend-form" v-if="$root.$data.os.isSignedIn && $root.$data.os.i.id != user.id">
+	<div class="friend-form" v-if="os.isSignedIn && os.i.id != user.id">
 		<mk-follow-button :user="user" size="big"/>
 		<p class="followed" v-if="user.is_followed">%i18n:desktop.tags.mk-user.follows-you%</p>
 		<p v-if="user.is_muted">%i18n:desktop.tags.mk-user.muted% <a @click="unmute">%i18n:desktop.tags.mk-user.unmute%</a></p>
@@ -35,7 +35,7 @@ export default Vue.extend({
 	methods: {
 		showFollowing() {
 			document.body.appendChild(new MkUserFollowingWindow({
-				parent: this,
+
 				propsData: {
 					user: this.user
 				}
@@ -44,7 +44,7 @@ export default Vue.extend({
 
 		showFollowers() {
 			document.body.appendChild(new MkUserFollowersWindow({
-				parent: this,
+
 				propsData: {
 					user: this.user
 				}
@@ -52,7 +52,7 @@ export default Vue.extend({
 		},
 
 		mute() {
-			this.$root.$data.os.api('mute/create', {
+			(this as any).api('mute/create', {
 				user_id: this.user.id
 			}).then(() => {
 				this.user.is_muted = true;
@@ -62,7 +62,7 @@ export default Vue.extend({
 		},
 
 		unmute() {
-			this.$root.$data.os.api('mute/delete', {
+			(this as any).api('mute/delete', {
 				user_id: this.user.id
 			}).then(() => {
 				this.user.is_muted = false;
diff --git a/src/web/app/desktop/views/pages/user/user.vue b/src/web/app/desktop/views/pages/user/user.vue
index 109ee6037..3339c2dce 100644
--- a/src/web/app/desktop/views/pages/user/user.vue
+++ b/src/web/app/desktop/views/pages/user/user.vue
@@ -29,7 +29,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		Progress.start();
-		this.$root.$data.os.api('users/show', {
+		(this as any).api('users/show', {
 			username: this.username
 		}).then(user => {
 			this.fetching = false;
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 450327a58..8abb7f7aa 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -78,37 +78,50 @@ if (localStorage.getItem('should-refresh') == 'true') {
 
 type API = {
 	chooseDriveFile: (opts: {
-		title: string;
-		currentFolder: any;
-		multiple: boolean;
+		title?: string;
+		currentFolder?: any;
+		multiple?: boolean;
 	}) => Promise<any>;
 
 	chooseDriveFolder: (opts: {
-		title: string;
-		currentFolder: any;
+		title?: string;
+		currentFolder?: any;
 	}) => Promise<any>;
+
+	dialog: (opts: {
+		title: string;
+		text: string;
+		actions: Array<{
+			text: string;
+			id: string;
+		}>;
+	}) => Promise<string>;
+
+	input: (opts: {
+		title: string;
+		placeholder?: string;
+		default?: string;
+	}) => Promise<string>;
 };
 
 // MiOSを初期化してコールバックする
 export default (callback: (launch: (api: API) => Vue) => void, sw = false) => {
-	const mios = new MiOS(sw);
+	const os = new MiOS(sw);
 
-	Vue.mixin({
-		data: {
-			$os: mios
-		}
-	});
-
-	mios.init(() => {
+	os.init(() => {
 		// アプリ基底要素マウント
 		document.body.innerHTML = '<div id="app"></div>';
 
 		const launch = (api: API) => {
+			Vue.mixin({
+				created() {
+					(this as any).os = os;
+					(this as any).api = os.api;
+					(this as any).apis = api;
+				}
+			});
+
 			return new Vue({
-				data: {
-					os: mios,
-					api: api
-				},
 				router: new VueRouter({
 					mode: 'history'
 				}),
@@ -124,7 +137,7 @@ export default (callback: (launch: (api: API) => Vue) => void, sw = false) => {
 
 		// 更新チェック
 		setTimeout(() => {
-			checkForUpdate(mios);
+			checkForUpdate(os);
 		}, 3000);
 	});
 };
diff --git a/src/web/app/mobile/views/components/drive.vue b/src/web/app/mobile/views/components/drive.vue
index c842caacb..e581d3f05 100644
--- a/src/web/app/mobile/views/components/drive.vue
+++ b/src/web/app/mobile/views/components/drive.vue
@@ -87,8 +87,8 @@ export default Vue.extend({
 		}
 	},
 	mounted() {
-		this.connection = this.$root.$data.os.streams.driveStream.getConnection();
-		this.connectionId = this.$root.$data.os.streams.driveStream.use();
+		this.connection = (this as any).os.streams.driveStream.getConnection();
+		this.connectionId = (this as any).os.streams.driveStream.use();
 
 		this.connection.on('file_created', this.onStreamDriveFileCreated);
 		this.connection.on('file_updated', this.onStreamDriveFileUpdated);
@@ -112,7 +112,7 @@ export default Vue.extend({
 		this.connection.off('file_updated', this.onStreamDriveFileUpdated);
 		this.connection.off('folder_created', this.onStreamDriveFolderCreated);
 		this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
-		this.$root.$data.os.streams.driveStream.dispose(this.connectionId);
+		(this as any).os.streams.driveStream.dispose(this.connectionId);
 	},
 	methods: {
 		onStreamDriveFileCreated(file) {
@@ -158,7 +158,7 @@ export default Vue.extend({
 
 			this.fetching = true;
 
-			this.$root.$data.os.api('drive/folders/show', {
+			(this as any).api('drive/folders/show', {
 				folder_id: target
 			}).then(folder => {
 				this.folder = folder;
@@ -253,7 +253,7 @@ export default Vue.extend({
 			const filesMax = 20;
 
 			// フォルダ一覧取得
-			this.$root.$data.os.api('drive/folders', {
+			(this as any).api('drive/folders', {
 				folder_id: this.folder ? this.folder.id : null,
 				limit: foldersMax + 1
 			}).then(folders => {
@@ -266,7 +266,7 @@ export default Vue.extend({
 			});
 
 			// ファイル一覧取得
-			this.$root.$data.os.api('drive/files', {
+			(this as any).api('drive/files', {
 				folder_id: this.folder ? this.folder.id : null,
 				limit: filesMax + 1
 			}).then(files => {
@@ -296,7 +296,7 @@ export default Vue.extend({
 
 			if (this.folder == null) {
 				// Fetch addtional drive info
-				this.$root.$data.os.api('drive').then(info => {
+				(this as any).api('drive').then(info => {
 					this.info = info;
 				});
 			}
@@ -309,7 +309,7 @@ export default Vue.extend({
 			const max = 30;
 
 			// ファイル一覧取得
-			this.$root.$data.os.api('drive/files', {
+			(this as any).api('drive/files', {
 				folder_id: this.folder ? this.folder.id : null,
 				limit: max + 1,
 				until_id: this.files[this.files.length - 1].id
@@ -348,7 +348,7 @@ export default Vue.extend({
 
 			this.fetching = true;
 
-			this.$root.$data.os.api('drive/files/show', {
+			(this as any).api('drive/files/show', {
 				file_id: file
 			}).then(file => {
 				this.fetching = false;
@@ -394,7 +394,7 @@ export default Vue.extend({
 		createFolder() {
 			const name = window.prompt('フォルダー名');
 			if (name == null || name == '') return;
-			this.$root.$data.os.api('drive/folders/create', {
+			(this as any).api('drive/folders/create', {
 				name: name,
 				parent_id: this.folder ? this.folder.id : undefined
 			}).then(folder => {
@@ -409,7 +409,7 @@ export default Vue.extend({
 			}
 			const name = window.prompt('フォルダー名', this.folder.name);
 			if (name == null || name == '') return;
-			this.$root.$data.os.api('drive/folders/update', {
+			(this as any).api('drive/folders/update', {
 				name: name,
 				folder_id: this.folder.id
 			}).then(folder => {
@@ -424,7 +424,7 @@ export default Vue.extend({
 			}
 			const dialog = riot.mount(document.body.appendChild(document.createElement('mk-drive-folder-selector')))[0];
 			dialog.one('selected', folder => {
-				this.$root.$data.os.api('drive/folders/update', {
+				(this as any).api('drive/folders/update', {
 					parent_id: folder ? folder.id : null,
 					folder_id: this.folder.id
 				}).then(folder => {
@@ -436,7 +436,7 @@ export default Vue.extend({
 		urlUpload() {
 			const url = window.prompt('アップロードしたいファイルのURL');
 			if (url == null || url == '') return;
-			this.$root.$data.os.api('drive/files/upload_from_url', {
+			(this as any).api('drive/files/upload_from_url', {
 				url: url,
 				folder_id: this.folder ? this.folder.id : undefined
 			});
diff --git a/src/web/app/mobile/views/components/follow-button.vue b/src/web/app/mobile/views/components/follow-button.vue
index 047005cc9..2d45ea215 100644
--- a/src/web/app/mobile/views/components/follow-button.vue
+++ b/src/web/app/mobile/views/components/follow-button.vue
@@ -28,8 +28,8 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.connection = this.$root.$data.os.stream.getConnection();
-		this.connectionId = this.$root.$data.os.stream.use();
+		this.connection = (this as any).os.stream.getConnection();
+		this.connectionId = (this as any).os.stream.use();
 
 		this.connection.on('follow', this.onFollow);
 		this.connection.on('unfollow', this.onUnfollow);
@@ -37,7 +37,7 @@ export default Vue.extend({
 	beforeDestroy() {
 		this.connection.off('follow', this.onFollow);
 		this.connection.off('unfollow', this.onUnfollow);
-		this.$root.$data.os.stream.dispose(this.connectionId);
+		(this as any).os.stream.dispose(this.connectionId);
 	},
 	methods: {
 
@@ -56,7 +56,7 @@ export default Vue.extend({
 		onClick() {
 			this.wait = true;
 			if (this.user.is_following) {
-				this.$root.$data.os.api('following/delete', {
+				(this as any).api('following/delete', {
 					user_id: this.user.id
 				}).then(() => {
 					this.user.is_following = false;
@@ -66,7 +66,7 @@ export default Vue.extend({
 					this.wait = false;
 				});
 			} else {
-				this.$root.$data.os.api('following/create', {
+				(this as any).api('following/create', {
 					user_id: this.user.id
 				}).then(() => {
 					this.user.is_following = true;
diff --git a/src/web/app/mobile/views/components/friends-maker.vue b/src/web/app/mobile/views/components/friends-maker.vue
index 45ee4a644..b069b988c 100644
--- a/src/web/app/mobile/views/components/friends-maker.vue
+++ b/src/web/app/mobile/views/components/friends-maker.vue
@@ -32,7 +32,7 @@ export default Vue.extend({
 			this.fetching = true;
 			this.users = [];
 
-			this.$root.$data.os.api('users/recommendation', {
+			(this as any).api('users/recommendation', {
 				limit: this.limit,
 				offset: this.limit * this.page
 			}).then(users => {
diff --git a/src/web/app/mobile/views/components/notifications.vue b/src/web/app/mobile/views/components/notifications.vue
index 8813bef5b..999dba404 100644
--- a/src/web/app/mobile/views/components/notifications.vue
+++ b/src/web/app/mobile/views/components/notifications.vue
@@ -42,14 +42,14 @@ export default Vue.extend({
 		}
 	},
 	mounted() {
-		this.connection = this.$root.$data.os.stream.getConnection();
-		this.connectionId = this.$root.$data.os.stream.use();
+		this.connection = (this as any).os.stream.getConnection();
+		this.connectionId = (this as any).os.stream.use();
 
 		this.connection.on('notification', this.onNotification);
 
 		const max = 10;
 
-		this.$root.$data.os.api('i/notifications', {
+		(this as any).api('i/notifications', {
 			limit: max + 1
 		}).then(notifications => {
 			if (notifications.length == max + 1) {
@@ -63,7 +63,7 @@ export default Vue.extend({
 	},
 	beforeDestroy() {
 		this.connection.off('notification', this.onNotification);
-		this.$root.$data.os.stream.dispose(this.connectionId);
+		(this as any).os.stream.dispose(this.connectionId);
 	},
 	methods: {
 		fetchMoreNotifications() {
@@ -71,7 +71,7 @@ export default Vue.extend({
 
 			const max = 30;
 
-			this.$root.$data.os.api('i/notifications', {
+			(this as any).api('i/notifications', {
 				limit: max + 1,
 				until_id: this.notifications[this.notifications.length - 1].id
 			}).then(notifications => {
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index da4f3fee7..87a591ff6 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -33,7 +33,7 @@
 			</div>
 		</header>
 		<div class="body">
-			<mk-post-html v-if="p.ast" :ast="p.ast" :i="$root.$data.os.i"/>
+			<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i"/>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			<div class="media" v-if="p.media">
 				<mk-images images={ p.media }/>
@@ -116,7 +116,7 @@ export default Vue.extend({
 	mounted() {
 		// Get replies
 		if (!this.compact) {
-			this.$root.$data.os.api('posts/replies', {
+			(this as any).api('posts/replies', {
 				post_id: this.p.id,
 				limit: 8
 			}).then(replies => {
@@ -129,7 +129,7 @@ export default Vue.extend({
 			this.contextFetching = true;
 
 			// Fetch context
-			this.$root.$data.os.api('posts/context', {
+			(this as any).api('posts/context', {
 				post_id: this.p.reply_id
 			}).then(context => {
 				this.contextFetching = false;
diff --git a/src/web/app/mobile/views/components/posts-post.vue b/src/web/app/mobile/views/components/posts-post.vue
index 56b42d9c2..b252a6e97 100644
--- a/src/web/app/mobile/views/components/posts-post.vue
+++ b/src/web/app/mobile/views/components/posts-post.vue
@@ -106,24 +106,24 @@ export default Vue.extend({
 		}
 	},
 	created() {
-		this.connection = this.$root.$data.os.stream.getConnection();
-		this.connectionId = this.$root.$data.os.stream.use();
+		this.connection = (this as any).os.stream.getConnection();
+		this.connectionId = (this as any).os.stream.use();
 	},
 	mounted() {
 		this.capture(true);
 
-		if (this.$root.$data.os.isSignedIn) {
+		if ((this as any).os.isSignedIn) {
 			this.connection.on('_connected_', this.onStreamConnected);
 		}
 	},
 	beforeDestroy() {
 		this.decapture(true);
 		this.connection.off('_connected_', this.onStreamConnected);
-		this.$root.$data.os.stream.dispose(this.connectionId);
+		(this as any).os.stream.dispose(this.connectionId);
 	},
 	methods: {
 		capture(withHandler = false) {
-			if (this.$root.$data.os.isSignedIn) {
+			if ((this as any).os.isSignedIn) {
 				this.connection.send({
 					type: 'capture',
 					id: this.post.id
@@ -132,7 +132,7 @@ export default Vue.extend({
 			}
 		},
 		decapture(withHandler = false) {
-			if (this.$root.$data.os.isSignedIn) {
+			if ((this as any).os.isSignedIn) {
 				this.connection.send({
 					type: 'decapture',
 					id: this.post.id
diff --git a/src/web/app/mobile/views/components/sub-post-content.vue b/src/web/app/mobile/views/components/sub-post-content.vue
index 48f3791aa..429e76005 100644
--- a/src/web/app/mobile/views/components/sub-post-content.vue
+++ b/src/web/app/mobile/views/components/sub-post-content.vue
@@ -2,7 +2,7 @@
 <div class="mk-sub-post-content">
 	<div class="body">
 		<a class="reply" v-if="post.reply_id">%fa:reply%</a>
-		<mk-post-html v-if="post.ast" :ast="post.ast" :i="$root.$data.os.i"/>
+		<mk-post-html v-if="post.ast" :ast="post.ast" :i="os.i"/>
 		<a class="quote" v-if="post.repost_id">RP: ...</a>
 	</div>
 	<details v-if="post.media">
diff --git a/src/web/app/mobile/views/components/timeline.vue b/src/web/app/mobile/views/components/timeline.vue
index 77c24a469..a04780e94 100644
--- a/src/web/app/mobile/views/components/timeline.vue
+++ b/src/web/app/mobile/views/components/timeline.vue
@@ -37,12 +37,12 @@ export default Vue.extend({
 	},
 	computed: {
 		alone(): boolean {
-			return this.$root.$data.os.i.following_count == 0;
+			return (this as any).os.i.following_count == 0;
 		}
 	},
 	mounted() {
-		this.connection = this.$root.$data.os.stream.getConnection();
-		this.connectionId = this.$root.$data.os.stream.use();
+		this.connection = (this as any).os.stream.getConnection();
+		this.connectionId = (this as any).os.stream.use();
 
 		this.connection.on('post', this.onPost);
 		this.connection.on('follow', this.onChangeFollowing);
@@ -54,13 +54,13 @@ export default Vue.extend({
 		this.connection.off('post', this.onPost);
 		this.connection.off('follow', this.onChangeFollowing);
 		this.connection.off('unfollow', this.onChangeFollowing);
-		this.$root.$data.os.stream.dispose(this.connectionId);
+		(this as any).os.stream.dispose(this.connectionId);
 	},
 	methods: {
 		fetch(cb?) {
 			this.fetching = true;
 
-			this.$root.$data.os.api('posts/timeline', {
+			(this as any).api('posts/timeline', {
 				until_date: this.date ? (this.date as any).getTime() : undefined
 			}).then(posts => {
 				this.fetching = false;
@@ -71,7 +71,7 @@ export default Vue.extend({
 		more() {
 			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
 			this.moreFetching = true;
-			this.$root.$data.os.api('posts/timeline', {
+			(this as any).api('posts/timeline', {
 				until_id: this.posts[this.posts.length - 1].id
 			}).then(posts => {
 				this.moreFetching = false;
diff --git a/src/web/app/mobile/views/components/ui-header.vue b/src/web/app/mobile/views/components/ui-header.vue
index 3bb1054c8..85fb45780 100644
--- a/src/web/app/mobile/views/components/ui-header.vue
+++ b/src/web/app/mobile/views/components/ui-header.vue
@@ -31,9 +31,9 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		if (this.$root.$data.os.isSignedIn) {
-			this.connection = this.$root.$data.os.stream.getConnection();
-			this.connectionId = this.$root.$data.os.stream.use();
+		if ((this as any).os.isSignedIn) {
+			this.connection = (this as any).os.stream.getConnection();
+			this.connectionId = (this as any).os.stream.use();
 
 			this.connection.on('read_all_notifications', this.onReadAllNotifications);
 			this.connection.on('unread_notification', this.onUnreadNotification);
@@ -41,14 +41,14 @@ export default Vue.extend({
 			this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
 
 			// Fetch count of unread notifications
-			this.$root.$data.os.api('notifications/get_unread_count').then(res => {
+			(this as any).api('notifications/get_unread_count').then(res => {
 				if (res.count > 0) {
 					this.hasUnreadNotifications = true;
 				}
 			});
 
 			// Fetch count of unread messaging messages
-			this.$root.$data.os.api('messaging/unread').then(res => {
+			(this as any).api('messaging/unread').then(res => {
 				if (res.count > 0) {
 					this.hasUnreadMessagingMessages = true;
 				}
@@ -56,12 +56,12 @@ export default Vue.extend({
 		}
 	},
 	beforeDestroy() {
-		if (this.$root.$data.os.isSignedIn) {
+		if ((this as any).os.isSignedIn) {
 			this.connection.off('read_all_notifications', this.onReadAllNotifications);
 			this.connection.off('unread_notification', this.onUnreadNotification);
 			this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
 			this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
-			this.$root.$data.os.stream.dispose(this.connectionId);
+			(this as any).os.stream.dispose(this.connectionId);
 		}
 	},
 	methods: {
diff --git a/src/web/app/mobile/views/components/ui-nav.vue b/src/web/app/mobile/views/components/ui-nav.vue
index cab24787d..1767e6224 100644
--- a/src/web/app/mobile/views/components/ui-nav.vue
+++ b/src/web/app/mobile/views/components/ui-nav.vue
@@ -2,7 +2,7 @@
 <div class="mk-ui-nav" :style="{ display: isOpen ? 'block' : 'none' }">
 	<div class="backdrop" @click="parent.toggleDrawer"></div>
 	<div class="body">
-		<a class="me" v-if="$root.$data.os.isSignedIn" href={ '/' + I.username }>
+		<a class="me" v-if="os.isSignedIn" href={ '/' + I.username }>
 			<img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/>
 			<p class="name">{ I.name }</p>
 		</a>
@@ -41,9 +41,9 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		if (this.$root.$data.os.isSignedIn) {
-			this.connection = this.$root.$data.os.stream.getConnection();
-			this.connectionId = this.$root.$data.os.stream.use();
+		if ((this as any).os.isSignedIn) {
+			this.connection = (this as any).os.stream.getConnection();
+			this.connectionId = (this as any).os.stream.use();
 
 			this.connection.on('read_all_notifications', this.onReadAllNotifications);
 			this.connection.on('unread_notification', this.onUnreadNotification);
@@ -51,14 +51,14 @@ export default Vue.extend({
 			this.connection.on('unread_messaging_message', this.onUnreadMessagingMessage);
 
 			// Fetch count of unread notifications
-			this.$root.$data.os.api('notifications/get_unread_count').then(res => {
+			(this as any).api('notifications/get_unread_count').then(res => {
 				if (res.count > 0) {
 					this.hasUnreadNotifications = true;
 				}
 			});
 
 			// Fetch count of unread messaging messages
-			this.$root.$data.os.api('messaging/unread').then(res => {
+			(this as any).api('messaging/unread').then(res => {
 				if (res.count > 0) {
 					this.hasUnreadMessagingMessages = true;
 				}
@@ -66,12 +66,12 @@ export default Vue.extend({
 		}
 	},
 	beforeDestroy() {
-		if (this.$root.$data.os.isSignedIn) {
+		if ((this as any).os.isSignedIn) {
 			this.connection.off('read_all_notifications', this.onReadAllNotifications);
 			this.connection.off('unread_notification', this.onUnreadNotification);
 			this.connection.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
 			this.connection.off('unread_messaging_message', this.onUnreadMessagingMessage);
-			this.$root.$data.os.stream.dispose(this.connectionId);
+			(this as any).os.stream.dispose(this.connectionId);
 		}
 	},
 	methods: {
diff --git a/src/web/app/mobile/views/components/ui.vue b/src/web/app/mobile/views/components/ui.vue
index 52443430a..a07c9ed5a 100644
--- a/src/web/app/mobile/views/components/ui.vue
+++ b/src/web/app/mobile/views/components/ui.vue
@@ -7,7 +7,7 @@
 	<div class="content">
 		<slot></slot>
 	</div>
-	<mk-stream-indicator v-if="$root.$data.os.isSignedIn"/>
+	<mk-stream-indicator v-if="os.isSignedIn"/>
 </div>
 </template>
 
@@ -23,17 +23,17 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		if (this.$root.$data.os.isSignedIn) {
-			this.connection = this.$root.$data.os.stream.getConnection();
-			this.connectionId = this.$root.$data.os.stream.use();
+		if ((this as any).os.isSignedIn) {
+			this.connection = (this as any).os.stream.getConnection();
+			this.connectionId = (this as any).os.stream.use();
 
 			this.connection.on('notification', this.onNotification);
 		}
 	},
 	beforeDestroy() {
-		if (this.$root.$data.os.isSignedIn) {
+		if ((this as any).os.isSignedIn) {
 			this.connection.off('notification', this.onNotification);
-			this.$root.$data.os.stream.dispose(this.connectionId);
+			(this as any).os.stream.dispose(this.connectionId);
 		}
 	},
 	methods: {
diff --git a/src/web/app/mobile/views/components/user-followers.vue b/src/web/app/mobile/views/components/user-followers.vue
index 22629af9d..771291b49 100644
--- a/src/web/app/mobile/views/components/user-followers.vue
+++ b/src/web/app/mobile/views/components/user-followers.vue
@@ -14,7 +14,7 @@ export default Vue.extend({
 	props: ['user'],
 	methods: {
 		fetch(iknow, limit, cursor, cb) {
-			this.$root.$data.os.api('users/followers', {
+			(this as any).api('users/followers', {
 				user_id: this.user.id,
 				iknow: iknow,
 				limit: limit,
diff --git a/src/web/app/mobile/views/components/user-following.vue b/src/web/app/mobile/views/components/user-following.vue
index bb739bc4c..dfd6135da 100644
--- a/src/web/app/mobile/views/components/user-following.vue
+++ b/src/web/app/mobile/views/components/user-following.vue
@@ -14,7 +14,7 @@ export default Vue.extend({
 	props: ['user'],
 	methods: {
 		fetch(iknow, limit, cursor, cb) {
-			this.$root.$data.os.api('users/following', {
+			(this as any).api('users/following', {
 				user_id: this.user.id,
 				iknow: iknow,
 				limit: limit,
diff --git a/src/web/app/mobile/views/components/user-timeline.vue b/src/web/app/mobile/views/components/user-timeline.vue
index 9a31ace4d..fb2a21419 100644
--- a/src/web/app/mobile/views/components/user-timeline.vue
+++ b/src/web/app/mobile/views/components/user-timeline.vue
@@ -27,7 +27,7 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.$root.$data.os.api('users/posts', {
+		(this as any).api('users/posts', {
 			user_id: this.user.id,
 			with_media: this.withMedia
 		}).then(posts => {
diff --git a/src/web/app/mobile/views/components/users-list.vue b/src/web/app/mobile/views/components/users-list.vue
index 54af40ec4..45629c558 100644
--- a/src/web/app/mobile/views/components/users-list.vue
+++ b/src/web/app/mobile/views/components/users-list.vue
@@ -2,7 +2,7 @@
 <div class="mk-users-list">
 	<nav>
 		<span :data-is-active="mode == 'all'" @click="mode = 'all'">%i18n:mobile.tags.mk-users-list.all%<span>{{ count }}</span></span>
-		<span v-if="$root.$data.os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:mobile.tags.mk-users-list.known%<span>{{ youKnowCount }}</span></span>
+		<span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:mobile.tags.mk-users-list.known%<span>{{ youKnowCount }}</span></span>
 	</nav>
 	<div class="users" v-if="!fetching && users.length != 0">
 		<mk-user-preview v-for="u in users" :user="u" :key="u.id"/>
diff --git a/src/web/app/mobile/views/pages/followers.vue b/src/web/app/mobile/views/pages/followers.vue
index dcaca16a2..e9696dbd3 100644
--- a/src/web/app/mobile/views/pages/followers.vue
+++ b/src/web/app/mobile/views/pages/followers.vue
@@ -23,7 +23,7 @@ export default Vue.extend({
 	mounted() {
 		Progress.start();
 
-		this.$root.$data.os.api('users/show', {
+		(this as any).api('users/show', {
 			username: this.username
 		}).then(user => {
 			this.fetching = false;
diff --git a/src/web/app/mobile/views/pages/following.vue b/src/web/app/mobile/views/pages/following.vue
index b11e3b95f..c278abfd2 100644
--- a/src/web/app/mobile/views/pages/following.vue
+++ b/src/web/app/mobile/views/pages/following.vue
@@ -23,7 +23,7 @@ export default Vue.extend({
 	mounted() {
 		Progress.start();
 
-		this.$root.$data.os.api('users/show', {
+		(this as any).api('users/show', {
 			username: this.username
 		}).then(user => {
 			this.fetching = false;
diff --git a/src/web/app/mobile/views/pages/home.vue b/src/web/app/mobile/views/pages/home.vue
index 3b069c614..4313ab699 100644
--- a/src/web/app/mobile/views/pages/home.vue
+++ b/src/web/app/mobile/views/pages/home.vue
@@ -23,8 +23,8 @@ export default Vue.extend({
 		document.title = 'Misskey';
 		document.documentElement.style.background = '#313a42';
 
-		this.connection = this.$root.$data.os.stream.getConnection();
-		this.connectionId = this.$root.$data.os.stream.use();
+		this.connection = (this as any).os.stream.getConnection();
+		this.connectionId = (this as any).os.stream.use();
 
 		this.connection.on('post', this.onStreamPost);
 		document.addEventListener('visibilitychange', this.onVisibilitychange, false);
@@ -33,7 +33,7 @@ export default Vue.extend({
 	},
 	beforeDestroy() {
 		this.connection.off('post', this.onStreamPost);
-		this.$root.$data.os.stream.dispose(this.connectionId);
+		(this as any).os.stream.dispose(this.connectionId);
 		document.removeEventListener('visibilitychange', this.onVisibilitychange);
 	},
 	methods: {
@@ -44,7 +44,7 @@ export default Vue.extend({
 			Progress.done();
 		},
 		onStreamPost(post) {
-			if (document.hidden && post.user_id !== this.$root.$data.os.i.id) {
+			if (document.hidden && post.user_id !== (this as any).os.i.id) {
 				this.unreadCount++;
 				document.title = `(${this.unreadCount}) ${getPostSummary(post)}`;
 			}
diff --git a/src/web/app/mobile/views/pages/notification.vue b/src/web/app/mobile/views/pages/notification.vue
index 03d8b6cad..0685bd127 100644
--- a/src/web/app/mobile/views/pages/notification.vue
+++ b/src/web/app/mobile/views/pages/notification.vue
@@ -21,7 +21,7 @@ export default Vue.extend({
 			const ok = window.confirm('%i18n:mobile.tags.mk-notifications-page.read-all%');
 			if (!ok) return;
 
-			this.$root.$data.os.api('notifications/mark_as_read_all');
+			(this as any).api('notifications/mark_as_read_all');
 		},
 		onFetched() {
 			Progress.done();
diff --git a/src/web/app/mobile/views/pages/post.vue b/src/web/app/mobile/views/pages/post.vue
index f291a489b..c5b6750af 100644
--- a/src/web/app/mobile/views/pages/post.vue
+++ b/src/web/app/mobile/views/pages/post.vue
@@ -29,7 +29,7 @@ export default Vue.extend({
 
 		Progress.start();
 
-		this.$root.$data.os.api('posts/show', {
+		(this as any).api('posts/show', {
 			post_id: this.postId
 		}).then(post => {
 			this.fetching = false;
diff --git a/src/web/app/mobile/views/pages/search.vue b/src/web/app/mobile/views/pages/search.vue
index 02cdb1600..b6e114a82 100644
--- a/src/web/app/mobile/views/pages/search.vue
+++ b/src/web/app/mobile/views/pages/search.vue
@@ -35,7 +35,7 @@ export default Vue.extend({
 
 		Progress.start();
 
-		this.$root.$data.os.api('posts/search', Object.assign({}, parse(this.query), {
+		(this as any).api('posts/search', Object.assign({}, parse(this.query), {
 			limit: limit
 		})).then(posts => {
 			this.posts = posts;
@@ -46,7 +46,7 @@ export default Vue.extend({
 	methods: {
 		more() {
 			this.offset += limit;
-			return this.$root.$data.os.api('posts/search', Object.assign({}, parse(this.query), {
+			return (this as any).api('posts/search', Object.assign({}, parse(this.query), {
 				limit: limit,
 				offset: this.offset
 			}));
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index 6c784b05f..f5babbd67 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -9,7 +9,7 @@
 					<a class="avatar">
 						<img :src="`${user.avatar_url}?thumbnail&size=200`" alt="avatar"/>
 					</a>
-					<mk-follow-button v-if="$root.$data.os.isSignedIn && $root.$data.os.i.id != user.id" :user="user"/>
+					<mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
 				</div>
 				<div class="title">
 					<h1>{{ user.name }}</h1>
@@ -85,7 +85,7 @@ export default Vue.extend({
 		document.documentElement.style.background = '#313a42';
 		Progress.start();
 
-		this.$root.$data.os.api('users/show', {
+		(this as any).api('users/show', {
 			username: this.username
 		}).then(user => {
 			this.fetching = false;
diff --git a/src/web/app/mobile/views/pages/user/followers-you-know.vue b/src/web/app/mobile/views/pages/user/followers-you-know.vue
index a4358f5d9..eb0ff68bd 100644
--- a/src/web/app/mobile/views/pages/user/followers-you-know.vue
+++ b/src/web/app/mobile/views/pages/user/followers-you-know.vue
@@ -21,7 +21,7 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.$root.$data.os.api('users/followers', {
+		(this as any).api('users/followers', {
 			user_id: this.user.id,
 			iknow: true,
 			limit: 30
diff --git a/src/web/app/mobile/views/pages/user/home-activity.vue b/src/web/app/mobile/views/pages/user/home-activity.vue
index 00a2dafc1..f38c5568e 100644
--- a/src/web/app/mobile/views/pages/user/home-activity.vue
+++ b/src/web/app/mobile/views/pages/user/home-activity.vue
@@ -28,7 +28,7 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.$root.$data.os.api('aggregation/users/activity', {
+		(this as any).api('aggregation/users/activity', {
 			user_id: this.user.id,
 			limit: 30
 		}).then(data => {
diff --git a/src/web/app/mobile/views/pages/user/home-friends.vue b/src/web/app/mobile/views/pages/user/home-friends.vue
index 7c5a50559..4f2f12a64 100644
--- a/src/web/app/mobile/views/pages/user/home-friends.vue
+++ b/src/web/app/mobile/views/pages/user/home-friends.vue
@@ -19,7 +19,7 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.$root.$data.os.api('users/get_frequently_replied_users', {
+		(this as any).api('users/get_frequently_replied_users', {
 			user_id: this.user.id
 		}).then(res => {
 			this.fetching = false;
diff --git a/src/web/app/mobile/views/pages/user/home-photos.vue b/src/web/app/mobile/views/pages/user/home-photos.vue
index fc2d0e139..eb53eb89a 100644
--- a/src/web/app/mobile/views/pages/user/home-photos.vue
+++ b/src/web/app/mobile/views/pages/user/home-photos.vue
@@ -23,7 +23,7 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.$root.$data.os.api('users/posts', {
+		(this as any).api('users/posts', {
 			user_id: this.user.id,
 			with_media: true,
 			limit: 6
diff --git a/src/web/app/mobile/views/pages/user/home-posts.vue b/src/web/app/mobile/views/pages/user/home-posts.vue
index b1451b088..c60f114b8 100644
--- a/src/web/app/mobile/views/pages/user/home-posts.vue
+++ b/src/web/app/mobile/views/pages/user/home-posts.vue
@@ -19,7 +19,7 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.$root.$data.os.api('users/posts', {
+		(this as any).api('users/posts', {
 			user_id: this.user.id
 		}).then(posts => {
 			this.fetching = false;
diff --git a/src/web/app/mobile/views/pages/user/home.vue b/src/web/app/mobile/views/pages/user/home.vue
index a23825f22..44ddd54dc 100644
--- a/src/web/app/mobile/views/pages/user/home.vue
+++ b/src/web/app/mobile/views/pages/user/home.vue
@@ -37,7 +37,7 @@
 			<mk-user-home-frequently-replied-users :user="user"/>
 		</div>
 	</section>
-	<section class="followers-you-know" v-if="$root.$data.os.isSignedIn && $root.$data.os.i.id !== user.id">
+	<section class="followers-you-know" v-if="os.isSignedIn && os.i.id !== user.id">
 		<h2>%fa:users%%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2>
 		<div>
 			<mk-user-home-followers-you-know :user="user"/>

From b2c24af69e50bad3e925805a32f68be34a511664 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 12:37:18 +0900
Subject: [PATCH 177/286] wip

---
 .../app/desktop/views/components/dialog.vue   | 28 ++++++++++---------
 1 file changed, 15 insertions(+), 13 deletions(-)

diff --git a/src/web/app/desktop/views/components/dialog.vue b/src/web/app/desktop/views/components/dialog.vue
index f2be5e443..af65d5d21 100644
--- a/src/web/app/desktop/views/components/dialog.vue
+++ b/src/web/app/desktop/views/components/dialog.vue
@@ -32,20 +32,22 @@ export default Vue.extend({
 		}
 	},
 	mounted() {
-		(this.$refs.bg as any).style.pointerEvents = 'auto';
-		anime({
-			targets: this.$refs.bg,
-			opacity: 1,
-			duration: 100,
-			easing: 'linear'
-		});
+		this.$nextTick(() => {
+			(this.$refs.bg as any).style.pointerEvents = 'auto';
+			anime({
+				targets: this.$refs.bg,
+				opacity: 1,
+				duration: 100,
+				easing: 'linear'
+			});
 
-		anime({
-			targets: this.$refs.main,
-			opacity: 1,
-			scale: [1.2, 1],
-			duration: 300,
-			easing: [0, 0.5, 0.5, 1]
+			anime({
+				targets: this.$refs.main,
+				opacity: 1,
+				scale: [1.2, 1],
+				duration: 300,
+				easing: [0, 0.5, 0.5, 1]
+			});
 		});
 	},
 	methods: {

From 2f3f97a12c3c20092ec17788aaabf01a7dab40df Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 13:48:40 +0900
Subject: [PATCH 178/286] wip

---
 .../desktop/views/components/context-menu-menu.vue    | 11 ++++++-----
 src/web/app/desktop/views/components/context-menu.vue |  2 +-
 src/web/app/desktop/views/components/dialog.vue       |  4 ++--
 3 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/web/app/desktop/views/components/context-menu-menu.vue b/src/web/app/desktop/views/components/context-menu-menu.vue
index 423ea0a1f..c4ecc74a4 100644
--- a/src/web/app/desktop/views/components/context-menu-menu.vue
+++ b/src/web/app/desktop/views/components/context-menu-menu.vue
@@ -4,6 +4,9 @@
 		<template v-if="item.type == 'item'">
 			<p @click="click(item)"><span class="icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</p>
 		</template>
+		<template v-if="item.type == 'link'">
+			<a :href="item.href" :target="item.target" @click="click(item)"><span class="icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</a>
+		</template>
 		<template v-else-if="item.type == 'nest'">
 			<p><span class="icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}...<span class="caret">%fa:caret-right%</span></p>
 			<me-nu :menu="item.menu" @x="click"/>
@@ -31,11 +34,9 @@ export default Vue.extend({
 	$item-height = 38px
 	$padding = 10px
 
-	ul
-		display block
-		margin 0
-		padding $padding 0
-		list-style none
+	margin 0
+	padding $padding 0
+	list-style none
 
 	li
 		display block
diff --git a/src/web/app/desktop/views/components/context-menu.vue b/src/web/app/desktop/views/components/context-menu.vue
index 9f5787e47..3ba475e11 100644
--- a/src/web/app/desktop/views/components/context-menu.vue
+++ b/src/web/app/desktop/views/components/context-menu.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="context-menu" :style="{ x: `${x}px`, y: `${y}px` }" @contextmenu.prevent="() => {}">
+<div class="context-menu" :style="{ left: `${x}px`, top: `${y}px` }" @contextmenu.prevent="() => {}">
 	<me-nu :menu="menu" @x="click"/>
 </div>
 </template>
diff --git a/src/web/app/desktop/views/components/dialog.vue b/src/web/app/desktop/views/components/dialog.vue
index af65d5d21..e92050dba 100644
--- a/src/web/app/desktop/views/components/dialog.vue
+++ b/src/web/app/desktop/views/components/dialog.vue
@@ -16,7 +16,7 @@ import Vue from 'vue';
 import * as anime from 'animejs';
 
 export default Vue.extend({
-	props: {
+	props: ['title', 'text', 'buttons', 'modal']/*{
 		title: {
 			type: String
 		},
@@ -30,7 +30,7 @@ export default Vue.extend({
 			type: Boolean,
 			default: false
 		}
-	},
+	}*/,
 	mounted() {
 		this.$nextTick(() => {
 			(this.$refs.bg as any).style.pointerEvents = 'auto';

From 62fd58fbc8e7c0ed3ab8452c66694920793abe15 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 15:27:06 +0900
Subject: [PATCH 179/286] wip

---
 package.json                                  |  3 +-
 .../views/components/context-menu-menu.vue    | 31 +++++++++++--------
 webpack/loaders/replace.js                    |  7 +++--
 webpack/plugins/index.ts                      |  2 +-
 webpack/webpack.config.ts                     | 17 +++++++++-
 5 files changed, 41 insertions(+), 19 deletions(-)

diff --git a/package.json b/package.json
index 6df445f29..e87be0ab2 100644
--- a/package.json
+++ b/package.json
@@ -83,6 +83,7 @@
 		"autwh": "0.0.1",
 		"bcryptjs": "2.4.3",
 		"body-parser": "1.18.2",
+		"cache-loader": "^1.2.0",
 		"cafy": "3.2.1",
 		"chai": "4.1.2",
 		"chai-http": "3.0.0",
@@ -117,7 +118,7 @@
 		"gulp-typescript": "3.2.4",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
-		"hard-source-webpack-plugin": "^0.5.18",
+		"hard-source-webpack-plugin": "0.6.0-alpha.8",
 		"highlight.js": "9.12.0",
 		"html-minifier": "^3.5.9",
 		"inquirer": "5.0.1",
diff --git a/src/web/app/desktop/views/components/context-menu-menu.vue b/src/web/app/desktop/views/components/context-menu-menu.vue
index c4ecc74a4..7e333d273 100644
--- a/src/web/app/desktop/views/components/context-menu-menu.vue
+++ b/src/web/app/desktop/views/components/context-menu-menu.vue
@@ -2,13 +2,13 @@
 <ul class="me-nu">
 	<li v-for="(item, i) in menu" :key="i" :class="item.type">
 		<template v-if="item.type == 'item'">
-			<p @click="click(item)"><span class="icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</p>
+			<p @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</p>
 		</template>
 		<template v-if="item.type == 'link'">
-			<a :href="item.href" :target="item.target" @click="click(item)"><span class="icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</a>
+			<a :href="item.href" :target="item.target" @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</a>
 		</template>
 		<template v-else-if="item.type == 'nest'">
-			<p><span class="icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}...<span class="caret">%fa:caret-right%</span></p>
+			<p><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}...<span class="caret">%fa:caret-right%</span></p>
 			<me-nu :menu="item.menu" @x="click"/>
 		</template>
 	</li>
@@ -41,7 +41,7 @@ export default Vue.extend({
 	li
 		display block
 
-		&:empty
+		&.divider
 			margin-top $padding
 			padding-top $padding
 			border-top solid 1px #eee
@@ -51,11 +51,14 @@ export default Vue.extend({
 				cursor default
 
 				> .caret
+					position absolute
+					top 0
+					right 8px
+
 					> *
-						position absolute
-						top 0
-						right 8px
 						line-height $item-height
+						width 28px
+						text-align center
 
 			&:hover > ul
 				visibility visible
@@ -80,12 +83,6 @@ export default Vue.extend({
 			*
 				pointer-events none
 
-			> .icon
-				> *
-					width 28px
-					margin-left -28px
-					text-align center
-
 		&:hover
 			> p, a
 				text-decoration none
@@ -112,3 +109,11 @@ export default Vue.extend({
 
 </style>
 
+<style lang="stylus" module>
+.icon
+	> *
+		width 28px
+		margin-left -28px
+		text-align center
+</style>
+
diff --git a/webpack/loaders/replace.js b/webpack/loaders/replace.js
index 4bb00a2ab..03cf1fcd7 100644
--- a/webpack/loaders/replace.js
+++ b/webpack/loaders/replace.js
@@ -1,17 +1,18 @@
 const loaderUtils = require('loader-utils');
 
-function trim(text) {
-	return text.substring(1, text.length - 2);
+function trim(text, g) {
+	return text.substring(1, text.length - (g ? 2 : 0));
 }
 
 module.exports = function(src) {
 	this.cacheable();
 	const options = loaderUtils.getOptions(this);
 	const search = options.search;
+	const g = search[search.length - 1] == 'g';
 	const replace = global[options.replace];
 	if (typeof search != 'string' || search.length == 0) console.error('invalid search');
 	if (typeof replace != 'function') console.error('invalid replacer:', replace, this.request);
-	src = src.replace(new RegExp(trim(search), 'g'), replace);
+	src = src.replace(new RegExp(trim(search, g), g ? 'g' : ''), replace);
 	this.callback(null, src);
 	return src;
 };
diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
index a29d2b7e2..027f60224 100644
--- a/webpack/plugins/index.ts
+++ b/webpack/plugins/index.ts
@@ -9,7 +9,7 @@ const isProduction = env === 'production';
 
 export default (version, lang) => {
 	const plugins = [
-		new HardSourceWebpackPlugin(),
+		//new HardSourceWebpackPlugin(),
 		consts(lang)
 	];
 
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 9a85e9189..fae75059a 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -2,6 +2,7 @@
  * webpack configuration
  */
 
+const minify = require('html-minifier').minify;
 import I18nReplacer from '../src/common/build/i18n';
 import { pattern as faPattern, replacement as faReplacement } from '../src/common/build/fa';
 const constants = require('../src/const.json');
@@ -13,6 +14,14 @@ import version from '../src/version';
 
 global['faReplacement'] = faReplacement;
 
+global['collapseSpacesReplacement'] = html => {
+	return minify(html, {
+		collapseWhitespace: true,
+		collapseInlineTagWhitespace: true,
+		keepClosingSlash: true
+	});
+};
+
 module.exports = Object.keys(langs).map(lang => {
 	// Chunk name
 	const name = lang;
@@ -44,7 +53,7 @@ module.exports = Object.keys(langs).map(lang => {
 			rules: [{
 				test: /\.vue$/,
 				exclude: /node_modules/,
-				use: [{
+				use: [/*'cache-loader', */{
 					loader: 'vue-loader',
 					options: {
 						cssSourceMap: false,
@@ -76,6 +85,12 @@ module.exports = Object.keys(langs).map(lang => {
 						search: faPattern.toString(),
 						replace: 'faReplacement'
 					}
+				}, {
+					loader: 'replace',
+					query: {
+						search: /^<template>([\s\S]+?)\r?\n<\/template>/.toString(),
+						replace: 'collapseSpacesReplacement'
+					}
 				}]
 			}, {
 				test: /\.styl$/,

From 8b6e9050c40868d8c5e201f88135b9e21b500cbc Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 16:38:07 +0900
Subject: [PATCH 180/286] wip

---
 .../desktop/views/components/choose-file-from-drive-window.vue  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/desktop/views/components/choose-file-from-drive-window.vue b/src/web/app/desktop/views/components/choose-file-from-drive-window.vue
index 5aa226f4c..89058bc3e 100644
--- a/src/web/app/desktop/views/components/choose-file-from-drive-window.vue
+++ b/src/web/app/desktop/views/components/choose-file-from-drive-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" is-modal width='800px' height='500px' @closed="$destroy">
+<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy">
 	<span slot="header">
 		<span v-html="title" :class="$style.title"></span>
 		<span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}ファイル選択中)</span>

From e82864d4fc63334560fbb2c324f180cb6d9cd3d6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 18:40:24 +0900
Subject: [PATCH 181/286] wip

---
 src/web/app/common/views/components/widgets/slideshow.vue | 2 +-
 src/web/app/common/views/components/widgets/tips.vue      | 2 +-
 src/web/app/desktop/views/components/context-menu.vue     | 6 +-----
 src/web/app/desktop/views/components/index.ts             | 2 ++
 src/web/app/desktop/views/components/input-dialog.vue     | 4 +++-
 src/web/app/desktop/views/components/post-form-window.vue | 2 +-
 src/web/app/desktop/views/components/post-form.vue        | 2 +-
 src/web/app/desktop/views/components/window.vue           | 2 +-
 src/web/app/mobile/views/components/notify.vue            | 2 +-
 9 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/src/web/app/common/views/components/widgets/slideshow.vue b/src/web/app/common/views/components/widgets/slideshow.vue
index a200aa061..1692cbc39 100644
--- a/src/web/app/common/views/components/widgets/slideshow.vue
+++ b/src/web/app/common/views/components/widgets/slideshow.vue
@@ -29,7 +29,7 @@ export default define({
 		};
 	},
 	mounted() {
-		Vue.nextTick(() => {
+		this.$nextTick(() => {
 			this.applySize();
 		});
 
diff --git a/src/web/app/common/views/components/widgets/tips.vue b/src/web/app/common/views/components/widgets/tips.vue
index f38ecfe44..28857f554 100644
--- a/src/web/app/common/views/components/widgets/tips.vue
+++ b/src/web/app/common/views/components/widgets/tips.vue
@@ -47,7 +47,7 @@ export default define({
 		};
 	},
 	mounted() {
-		Vue.nextTick(() => {
+		this.$nextTick(() => {
 			this.set();
 		});
 
diff --git a/src/web/app/desktop/views/components/context-menu.vue b/src/web/app/desktop/views/components/context-menu.vue
index 3ba475e11..9238b4246 100644
--- a/src/web/app/desktop/views/components/context-menu.vue
+++ b/src/web/app/desktop/views/components/context-menu.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="context-menu" :style="{ left: `${x}px`, top: `${y}px` }" @contextmenu.prevent="() => {}">
-	<me-nu :menu="menu" @x="click"/>
+	<context-menu-menu :menu="menu" @x="click"/>
 </div>
 </template>
 
@@ -8,12 +8,8 @@
 import Vue from 'vue';
 import * as anime from 'animejs';
 import contains from '../../../common/scripts/contains';
-import meNu from './context-menu-menu.vue';
 
 export default Vue.extend({
-	components: {
-		'me-nu': meNu
-	},
 	props: ['x', 'y', 'menu'],
 	mounted() {
 		this.$nextTick(() => {
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 1e4bd96a1..2ec368cf1 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -31,6 +31,7 @@ import drive from './drive.vue';
 import driveFile from './drive-file.vue';
 import driveFolder from './drive-folder.vue';
 import driveNavFolder from './drive-nav-folder.vue';
+import contextMenuMenu from './context-menu-menu.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-header', uiHeader);
@@ -63,3 +64,4 @@ Vue.component('mk-drive', drive);
 Vue.component('mk-drive-file', driveFile);
 Vue.component('mk-drive-folder', driveFolder);
 Vue.component('mk-drive-nav-folder', driveNavFolder);
+Vue.component('context-menu-menu', contextMenuMenu);
diff --git a/src/web/app/desktop/views/components/input-dialog.vue b/src/web/app/desktop/views/components/input-dialog.vue
index c00b1d4c1..99a9df106 100644
--- a/src/web/app/desktop/views/components/input-dialog.vue
+++ b/src/web/app/desktop/views/components/input-dialog.vue
@@ -43,7 +43,9 @@ export default Vue.extend({
 	},
 	mounted() {
 		if (this.default) this.text = this.default;
-		(this.$refs.text as any).focus();
+		this.$nextTick(() => {
+			(this.$refs.text as any).focus();
+		});
 	},
 	methods: {
 		ok() {
diff --git a/src/web/app/desktop/views/components/post-form-window.vue b/src/web/app/desktop/views/components/post-form-window.vue
index 8647a8d2d..4427f5982 100644
--- a/src/web/app/desktop/views/components/post-form-window.vue
+++ b/src/web/app/desktop/views/components/post-form-window.vue
@@ -28,7 +28,7 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		Vue.nextTick(() => {
+		this.$nextTick(() => {
 			(this.$refs.form as any).focus();
 		});
 	},
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 456f0de82..f117f8cc5 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -82,7 +82,7 @@ export default Vue.extend({
 		}
 	},
 	mounted() {
-		Vue.nextTick(() => {
+		this.$nextTick(() => {
 			this.autocomplete = new Autocomplete(this.$refs.text);
 			this.autocomplete.attach();
 
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 08e28007a..7f7f77813 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -84,7 +84,7 @@ export default Vue.extend({
 	},
 
 	mounted() {
-		Vue.nextTick(() => {
+		this.$nextTick(() => {
 			const main = this.$refs.main as any;
 			main.style.top = '15%';
 			main.style.left = (window.innerWidth / 2) - (main.offsetWidth / 2) + 'px';
diff --git a/src/web/app/mobile/views/components/notify.vue b/src/web/app/mobile/views/components/notify.vue
index d3e09e450..6d4a481db 100644
--- a/src/web/app/mobile/views/components/notify.vue
+++ b/src/web/app/mobile/views/components/notify.vue
@@ -11,7 +11,7 @@ import * as anime from 'animejs';
 export default Vue.extend({
 	props: ['notification'],
 	mounted() {
-		Vue.nextTick(() => {
+		this.$nextTick(() => {
 			anime({
 				targets: this.$el,
 				bottom: '0px',

From 455f70ef505824eb63847c3258f95c2da59845f2 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 18:46:33 +0900
Subject: [PATCH 182/286] wip

---
 src/web/app/common/views/components/widgets/slideshow.vue | 1 -
 src/web/app/common/views/components/widgets/tips.vue      | 1 -
 2 files changed, 2 deletions(-)

diff --git a/src/web/app/common/views/components/widgets/slideshow.vue b/src/web/app/common/views/components/widgets/slideshow.vue
index 1692cbc39..ea8e38a2c 100644
--- a/src/web/app/common/views/components/widgets/slideshow.vue
+++ b/src/web/app/common/views/components/widgets/slideshow.vue
@@ -11,7 +11,6 @@
 </template>
 
 <script lang="ts">
-import Vue from 'vue';
 import * as anime from 'animejs';
 import define from '../../../define-widget';
 export default define({
diff --git a/src/web/app/common/views/components/widgets/tips.vue b/src/web/app/common/views/components/widgets/tips.vue
index 28857f554..d9e1fbc94 100644
--- a/src/web/app/common/views/components/widgets/tips.vue
+++ b/src/web/app/common/views/components/widgets/tips.vue
@@ -5,7 +5,6 @@
 </template>
 
 <script lang="ts">
-import Vue from 'vue';
 import * as anime from 'animejs';
 import define from '../../../define-widget';
 

From 211b62eb9203f4139a7b125bb6e04fa03d889bdd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 18:49:44 +0900
Subject: [PATCH 183/286] wip

---
 webpack/plugins/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
index 027f60224..a29d2b7e2 100644
--- a/webpack/plugins/index.ts
+++ b/webpack/plugins/index.ts
@@ -9,7 +9,7 @@ const isProduction = env === 'production';
 
 export default (version, lang) => {
 	const plugins = [
-		//new HardSourceWebpackPlugin(),
+		new HardSourceWebpackPlugin(),
 		consts(lang)
 	];
 

From 43ee5267c2111257889849bcf0ead7fc82a326db Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 22:14:51 +0900
Subject: [PATCH 184/286] wip

---
 src/web/app/desktop/api/input.ts                 |  3 ++-
 .../components/choose-file-from-drive-window.vue |  2 +-
 .../app/desktop/views/components/drive-file.vue  |  3 ++-
 .../views/components/drive-nav-folder.vue        |  7 ++++++-
 .../desktop/views/components/input-dialog.vue    | 16 ++++++++--------
 5 files changed, 19 insertions(+), 12 deletions(-)

diff --git a/src/web/app/desktop/api/input.ts b/src/web/app/desktop/api/input.ts
index a5ab07138..ce26a8112 100644
--- a/src/web/app/desktop/api/input.ts
+++ b/src/web/app/desktop/api/input.ts
@@ -8,7 +8,8 @@ export default function(opts) {
 				title: o.title,
 				placeholder: o.placeholder,
 				default: o.default,
-				type: o.type || 'text'
+				type: o.type || 'text',
+				allowEmpty: o.allowEmpty
 			}
 		}).$mount();
 		d.$once('done', text => {
diff --git a/src/web/app/desktop/views/components/choose-file-from-drive-window.vue b/src/web/app/desktop/views/components/choose-file-from-drive-window.vue
index 89058bc3e..232282745 100644
--- a/src/web/app/desktop/views/components/choose-file-from-drive-window.vue
+++ b/src/web/app/desktop/views/components/choose-file-from-drive-window.vue
@@ -41,7 +41,7 @@ export default Vue.extend({
 			this.files = [file];
 			this.ok();
 		},
-		onChangeselection(files) {
+		onChangeSelection(files) {
 			this.files = files;
 		},
 		upload() {
diff --git a/src/web/app/desktop/views/components/drive-file.vue b/src/web/app/desktop/views/components/drive-file.vue
index 0681b5f03..772b9baf5 100644
--- a/src/web/app/desktop/views/components/drive-file.vue
+++ b/src/web/app/desktop/views/components/drive-file.vue
@@ -148,7 +148,8 @@ export default Vue.extend({
 			(this as any).apis.input({
 				title: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.rename-file%',
 				placeholder: '%i18n:desktop.tags.mk-drive-browser-file-contextmenu.input-new-file-name%',
-				default: this.file.name
+				default: this.file.name,
+				allowEmpty: false
 			}).then(name => {
 				(this as any).api('drive/files/update', {
 					file_id: this.file.id,
diff --git a/src/web/app/desktop/views/components/drive-nav-folder.vue b/src/web/app/desktop/views/components/drive-nav-folder.vue
index b6eb36535..44821087a 100644
--- a/src/web/app/desktop/views/components/drive-nav-folder.vue
+++ b/src/web/app/desktop/views/components/drive-nav-folder.vue
@@ -15,13 +15,18 @@
 <script lang="ts">
 import Vue from 'vue';
 export default Vue.extend({
-	props: ['folder', 'browser'],
+	props: ['folder'],
 	data() {
 		return {
 			hover: false,
 			draghover: false
 		};
 	},
+	computed: {
+		browser(): any {
+			return this.$parent;
+		}
+	},
 	methods: {
 		onClick() {
 			this.browser.move(this.folder);
diff --git a/src/web/app/desktop/views/components/input-dialog.vue b/src/web/app/desktop/views/components/input-dialog.vue
index 99a9df106..a735ce0f3 100644
--- a/src/web/app/desktop/views/components/input-dialog.vue
+++ b/src/web/app/desktop/views/components/input-dialog.vue
@@ -3,14 +3,13 @@
 	<span slot="header" :class="$style.header">
 		%fa:i-cursor%{{ title }}
 	</span>
-	<div slot="content">
-		<div :class="$style.body">
-			<input ref="text" v-model="text" :type="type" @keydown="onKeydown" :placeholder="placeholder"/>
-		</div>
-		<div :class="$style.actions">
-			<button :class="$style.cancel" @click="cancel">キャンセル</button>
-			<button :class="$style.ok" disabled="!allowEmpty && text.length == 0" @click="ok">決定</button>
-		</div>
+
+	<div :class="$style.body">
+		<input ref="text" v-model="text" :type="type" @keydown="onKeydown" :placeholder="placeholder"/>
+	</div>
+	<div :class="$style.actions">
+		<button :class="$style.cancel" @click="cancel">キャンセル</button>
+		<button :class="$style.ok" :disabled="!allowEmpty && text.length == 0" @click="ok">決定</button>
 	</div>
 </mk-window>
 </template>
@@ -44,6 +43,7 @@ export default Vue.extend({
 	mounted() {
 		if (this.default) this.text = this.default;
 		this.$nextTick(() => {
+			console.log(this);
 			(this.$refs.text as any).focus();
 		});
 	},

From 2a54802efa9b487dff720fb964ce818b6d797c18 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 22:16:36 +0900
Subject: [PATCH 185/286] wip

---
 src/web/app/desktop/views/directives/user-preview.ts | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/web/app/desktop/views/directives/user-preview.ts b/src/web/app/desktop/views/directives/user-preview.ts
index 322302bcf..6e800ee73 100644
--- a/src/web/app/desktop/views/directives/user-preview.ts
+++ b/src/web/app/desktop/views/directives/user-preview.ts
@@ -1,3 +1,7 @@
+/**
+ * マウスオーバーするとユーザーがプレビューされる要素を設定します
+ */
+
 import MkUserPreview from '../components/user-preview.vue';
 
 export default {
@@ -19,25 +23,31 @@ export default {
 
 		const show = () => {
 			if (tag) return;
+
 			tag = new MkUserPreview({
 				parent: vn.context,
 				propsData: {
 					user: self.user
 				}
 			}).$mount();
+
 			const preview = tag.$el;
 			const rect = el.getBoundingClientRect();
 			const x = rect.left + el.offsetWidth + window.pageXOffset;
 			const y = rect.top + window.pageYOffset;
+
 			preview.style.top = y + 'px';
 			preview.style.left = x + 'px';
+
 			preview.addEventListener('mouseover', () => {
 				clearTimeout(self.hideTimer);
 			});
+
 			preview.addEventListener('mouseleave', () => {
 				clearTimeout(self.showTimer);
 				self.hideTimer = setTimeout(self.close, 500);
 			});
+
 			document.body.appendChild(preview);
 		};
 
@@ -53,6 +63,7 @@ export default {
 			self.hideTimer = setTimeout(self.close, 500);
 		});
 	},
+
 	unbind(el, binding, vn) {
 		const self = vn.context._userPreviewDirective_;
 		clearTimeout(self.showTimer);

From e01b9d3f16dacb6504c69a65b2f3c3e0f85c4094 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 23:51:41 +0900
Subject: [PATCH 186/286] wip

---
 src/web/app/common/define-widget.ts           | 23 ++----
 src/web/app/common/views/components/index.ts  | 22 +++---
 .../views/components/messaging-form.vue       | 18 ++---
 .../app/common/views/components/messaging.vue |  2 +-
 .../-tags/home-widgets/notifications.tag      | 66 -----------------
 .../desktop/views/components/drive-window.vue | 10 +--
 .../views/components/follow-button.vue        |  1 -
 src/web/app/desktop/views/components/home.vue | 12 ++--
 src/web/app/desktop/views/components/index.ts | 14 ++++
 .../views/components/messaging-window.vue     |  4 +-
 .../views/components/settings-window.vue      |  5 ++
 .../views/components/ui-header-account.vue    |  4 +-
 .../views/components/ui-header-nav.vue        |  4 +-
 .../views/components/widgets/calendar.vue     |  2 +-
 .../views/components/widgets/donation.vue     |  2 +-
 .../views/components/widgets/messaging.vue    |  2 +-
 .../views/components/widgets/nav.vue          |  2 +-
 .../components/widgets/notifications.vue      | 70 +++++++++++++++++++
 .../views/components/widgets/photo-stream.vue |  2 +-
 .../views/components/widgets/profile.vue      |  2 +-
 .../views/components/widgets/slideshow.vue    |  2 +-
 .../views/components/widgets/tips.vue         |  2 +-
 22 files changed, 142 insertions(+), 129 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/notifications.tag
 rename src/web/app/{common => desktop}/views/components/widgets/calendar.vue (98%)
 rename src/web/app/{common => desktop}/views/components/widgets/donation.vue (94%)
 rename src/web/app/{common => desktop}/views/components/widgets/nav.vue (86%)
 create mode 100644 src/web/app/desktop/views/components/widgets/notifications.vue
 rename src/web/app/{common => desktop}/views/components/widgets/photo-stream.vue (97%)
 rename src/web/app/{common => desktop}/views/components/widgets/profile.vue (97%)
 rename src/web/app/{common => desktop}/views/components/widgets/slideshow.vue (98%)
 rename src/web/app/{common => desktop}/views/components/widgets/tips.vue (98%)

diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index 4e83e37c6..6088efd7e 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -6,22 +6,13 @@ export default function<T extends object>(data: {
 }) {
 	return Vue.extend({
 		props: {
-			wid: {
-				type: String,
-				required: true
-			},
-			wplace: {
-				type: String,
-				required: true
-			},
-			wprops: {
-				type: Object,
-				required: false
+			widget: {
+				type: Object
 			}
 		},
 		computed: {
 			id(): string {
-				return this.wid;
+				return this.widget.id;
 			}
 		},
 		data() {
@@ -32,19 +23,19 @@ export default function<T extends object>(data: {
 		watch: {
 			props(newProps, oldProps) {
 				if (JSON.stringify(newProps) == JSON.stringify(oldProps)) return;
-				this.$root.$data.os.api('i/update_home', {
+				(this as any).api('i/update_home', {
 					id: this.id,
 					data: newProps
 				}).then(() => {
-					this.$root.$data.os.i.client_settings.home.find(w => w.id == this.id).data = newProps;
+					(this as any).os.i.client_settings.home.find(w => w.id == this.id).data = newProps;
 				});
 			}
 		},
 		created() {
 			if (this.props) {
 				Object.keys(this.props).forEach(prop => {
-					if (this.wprops.hasOwnProperty(prop)) {
-						this.props[prop] = this.wprops[prop];
+					if (this.widget.data.hasOwnProperty(prop)) {
+						this.props[prop] = this.widget.data[prop];
 					}
 				});
 			}
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 209a68fe5..646fa3b71 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -5,6 +5,7 @@ import signup from './signup.vue';
 import forkit from './forkit.vue';
 import nav from './nav.vue';
 import postHtml from './post-html';
+import pollEditor from './poll-editor.vue';
 import reactionIcon from './reaction-icon.vue';
 import reactionsViewer from './reactions-viewer.vue';
 import time from './time.vue';
@@ -13,18 +14,17 @@ import uploader from './uploader.vue';
 import specialMessage from './special-message.vue';
 import streamIndicator from './stream-indicator.vue';
 import ellipsis from './ellipsis.vue';
-import wNav from './widgets/nav.vue';
-import wCalendar from './widgets/calendar.vue';
-import wPhotoStream from './widgets/photo-stream.vue';
-import wSlideshow from './widgets/slideshow.vue';
-import wTips from './widgets/tips.vue';
-import wDonation from './widgets/donation.vue';
+import messaging from './messaging.vue';
+import messagingForm from './messaging-form.vue';
+import messagingRoom from './messaging-room.vue';
+import messagingMessage from './messaging-message.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
 Vue.component('mk-forkit', forkit);
 Vue.component('mk-nav', nav);
 Vue.component('mk-post-html', postHtml);
+Vue.component('mk-poll-editor', pollEditor);
 Vue.component('mk-reaction-icon', reactionIcon);
 Vue.component('mk-reactions-viewer', reactionsViewer);
 Vue.component('mk-time', time);
@@ -33,9 +33,7 @@ Vue.component('mk-uploader', uploader);
 Vue.component('mk-special-message', specialMessage);
 Vue.component('mk-stream-indicator', streamIndicator);
 Vue.component('mk-ellipsis', ellipsis);
-Vue.component('mkw-nav', wNav);
-Vue.component('mkw-calendar', wCalendar);
-Vue.component('mkw-photo-stream', wPhotoStream);
-Vue.component('mkw-slideshoe', wSlideshow);
-Vue.component('mkw-tips', wTips);
-Vue.component('mkw-donation', wDonation);
+Vue.component('mk-messaging', messaging);
+Vue.component('mk-messaging-form', messagingForm);
+Vue.component('mk-messaging-room', messagingRoom);
+Vue.component('mk-messaging-message', messagingMessage);
diff --git a/src/web/app/common/views/components/messaging-form.vue b/src/web/app/common/views/components/messaging-form.vue
index 18d45790e..37ac51509 100644
--- a/src/web/app/common/views/components/messaging-form.vue
+++ b/src/web/app/common/views/components/messaging-form.vue
@@ -23,7 +23,7 @@ export default Vue.extend({
 	data() {
 		return {
 			text: null,
-			files: [],
+			file: null,
 			sending: false
 		};
 	},
@@ -49,17 +49,17 @@ export default Vue.extend({
 		},
 
 		chooseFileFromDrive() {
-			const w = new MkDriveChooserWindow({
-				propsData: {
-					multiple: true
-				}
-			}).$mount();
-			w.$once('selected', files => {
-				files.forEach(this.addFile);
+			(this as any).apis.chooseDriveFile({
+				multiple: false
+			}).then(file => {
+				this.file = file;
 			});
-			document.body.appendChild(w.$el);
 		},
 
+		upload() {
+			// TODO
+		}
+
 		send() {
 			this.sending = true;
 			(this as any).api('messaging/messages/create', {
diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index 1b56382b0..c0b3a1924 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-messaging" :data-compact="compact">
-	<div class="search" v-if="!opts.compact">
+	<div class="search" v-if="!compact">
 		<div class="form">
 			<label for="search-input">%fa:search%</label>
 			<input v-model="q" type="search" @input="search" @keydown="onSearchKeydown" placeholder="%i18n:common.tags.mk-messaging.search-user%"/>
diff --git a/src/web/app/desktop/-tags/home-widgets/notifications.tag b/src/web/app/desktop/-tags/home-widgets/notifications.tag
deleted file mode 100644
index bd915b197..000000000
--- a/src/web/app/desktop/-tags/home-widgets/notifications.tag
+++ /dev/null
@@ -1,66 +0,0 @@
-<mk-notifications-home-widget>
-	<template v-if="!data.compact">
-		<p class="title">%fa:R bell%%i18n:desktop.tags.mk-notifications-home-widget.title%</p>
-		<button @click="settings" title="%i18n:desktop.tags.mk-notifications-home-widget.settings%">%fa:cog%</button>
-	</template>
-	<mk-notifications/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> .title
-				z-index 1
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> [data-fa]
-					margin-right 4px
-
-			> button
-				position absolute
-				z-index 2
-				top 0
-				right 0
-				padding 0
-				width 42px
-				font-size 0.9em
-				line-height 42px
-				color #ccc
-
-				&:hover
-					color #aaa
-
-				&:active
-					color #999
-
-			> mk-notifications
-				max-height 300px
-				overflow auto
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			compact: false
-		};
-
-		this.mixin('widget');
-
-		this.settings = () => {
-			const w = riot.mount(document.body.appendChild(document.createElement('mk-settings-window')))[0];
-			w.switch('notification');
-		};
-
-		this.func = () => {
-			this.data.compact = !this.data.compact;
-			this.save();
-		};
-	</script>
-</mk-notifications-home-widget>
diff --git a/src/web/app/desktop/views/components/drive-window.vue b/src/web/app/desktop/views/components/drive-window.vue
index 0f0d8d81b..309ae14b5 100644
--- a/src/web/app/desktop/views/components/drive-window.vue
+++ b/src/web/app/desktop/views/components/drive-window.vue
@@ -1,9 +1,9 @@
 <template>
 <mk-window ref="window" @closed="$destroy" width="800px" height="500px" :popout="popout">
-	<span slot="header" :class="$style.header">
-		<p class="info" v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:desktop.tags.mk-drive-browser-window.used%</p>
-		%fa:cloud%%i18n:desktop.tags.mk-drive-browser-window.drive%
-	</span>
+	<template slot="header">
+		<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:desktop.tags.mk-drive-browser-window.used%</p>
+		<span: class="$style.title">%fa:cloud%%i18n:desktop.tags.mk-drive-browser-window.drive%</span>
+	</template>
 	<mk-drive-browser multiple :folder="folder" ref="browser"/>
 </mk-window>
 </template>
@@ -38,7 +38,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" module>
-.header
+.title
 	> [data-fa]
 		margin-right 4px
 
diff --git a/src/web/app/desktop/views/components/follow-button.vue b/src/web/app/desktop/views/components/follow-button.vue
index c4c3063ae..4697fb05e 100644
--- a/src/web/app/desktop/views/components/follow-button.vue
+++ b/src/web/app/desktop/views/components/follow-button.vue
@@ -1,7 +1,6 @@
 <template>
 <button class="mk-follow-button"
 	:class="{ wait, follow: !user.is_following, unfollow: user.is_following }"
-	v-if="!init"
 	@click="onClick"
 	:disabled="wait"
 	:title="user.is_following ? 'フォロー解除' : 'フォローする'"
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index f5f33e587..3a04e13cb 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -41,10 +41,10 @@
 			<div ref="left" data-place="left">
 				<template v-for="widget in leftWidgets">
 					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
-						<component :is="'mk-hw-' + widget.name" :widget="widget" :ref="widget.id"/>
+						<component :is="'mkw-' + widget.name" :widget="widget" :ref="widget.id"/>
 					</div>
 					<template v-else>
-						<component :is="'mk-hw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
+						<component :is="'mkw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
 					</template>
 				</template>
 			</div>
@@ -53,10 +53,10 @@
 			<div class="maintop" ref="maintop" data-place="main" v-if="customize">
 				<template v-for="widget in centerWidgets">
 					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
-						<component :is="'mk-hw-' + widget.name" :widget="widget" :ref="widget.id"/>
+						<component :is="'mkw-' + widget.name" :widget="widget" :ref="widget.id"/>
 					</div>
 					<template v-else>
-						<component :is="'mk-hw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
+						<component :is="'mkw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
 					</template>
 				</template>
 			</div>
@@ -67,10 +67,10 @@
 			<div ref="right" data-place="right">
 				<template v-for="widget in rightWidgets">
 					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
-						<component :is="'mk-hw-' + widget.name" :widget="widget" :ref="widget.id"/>
+						<component :is="'mkw-' + widget.name" :widget="widget" :ref="widget.id"/>
 					</div>
 					<template v-else>
-						<component :is="'mk-hw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
+						<component :is="'mkw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
 					</template>
 				</template>
 			</div>
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 2ec368cf1..4b390ffdd 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -32,6 +32,13 @@ import driveFile from './drive-file.vue';
 import driveFolder from './drive-folder.vue';
 import driveNavFolder from './drive-nav-folder.vue';
 import contextMenuMenu from './context-menu-menu.vue';
+import wNav from './widgets/nav.vue';
+import wCalendar from './widgets/calendar.vue';
+import wPhotoStream from './widgets/photo-stream.vue';
+import wSlideshow from './widgets/slideshow.vue';
+import wTips from './widgets/tips.vue';
+import wDonation from './widgets/donation.vue';
+import wNotifications from './widgets/notifications.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-header', uiHeader);
@@ -65,3 +72,10 @@ Vue.component('mk-drive-file', driveFile);
 Vue.component('mk-drive-folder', driveFolder);
 Vue.component('mk-drive-nav-folder', driveNavFolder);
 Vue.component('context-menu-menu', contextMenuMenu);
+Vue.component('mkw-nav', wNav);
+Vue.component('mkw-calendar', wCalendar);
+Vue.component('mkw-photo-stream', wPhotoStream);
+Vue.component('mkw-slideshoe', wSlideshow);
+Vue.component('mkw-tips', wTips);
+Vue.component('mkw-donation', wDonation);
+Vue.component('mkw-notifications', wNotifications);
diff --git a/src/web/app/desktop/views/components/messaging-window.vue b/src/web/app/desktop/views/components/messaging-window.vue
index 0dbcddbec..eeeb97e34 100644
--- a/src/web/app/desktop/views/components/messaging-window.vue
+++ b/src/web/app/desktop/views/components/messaging-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" width='500px' height='560px' @closed="$destroy">
+<mk-window ref="window" width="500px" height="560px" @closed="$destroy">
 	<span slot="header" :class="$style.header">%fa:comments%メッセージ</span>
 	<mk-messaging :class="$style.content" @navigate="navigate"/>
 </mk-window>
@@ -7,6 +7,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import MkMessagingRoomWindow from './messaging-room-window.vue';
+
 export default Vue.extend({
 	methods: {
 		navigate(user) {
diff --git a/src/web/app/desktop/views/components/settings-window.vue b/src/web/app/desktop/views/components/settings-window.vue
index 074bd2e24..9b264da0f 100644
--- a/src/web/app/desktop/views/components/settings-window.vue
+++ b/src/web/app/desktop/views/components/settings-window.vue
@@ -7,6 +7,11 @@
 </mk-window>
 </template>
 
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({});
+</script>
+
 <style lang="stylus" module>
 .header
 	> [data-fa]
diff --git a/src/web/app/desktop/views/components/ui-header-account.vue b/src/web/app/desktop/views/components/ui-header-account.vue
index 420fa6994..337c47674 100644
--- a/src/web/app/desktop/views/components/ui-header-account.vue
+++ b/src/web/app/desktop/views/components/ui-header-account.vue
@@ -33,6 +33,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import MkSettingsWindow from './settings-window.vue';
+import MkDriveWindow from './drive-window.vue';
 import contains from '../../../common/scripts/contains';
 import signout from '../../../common/scripts/signout';
 
@@ -69,8 +70,7 @@ export default Vue.extend({
 		},
 		drive() {
 			this.close();
-			// TODO
-			//document.body.appendChild(new MkDriveWindow().$mount().$el);
+			document.body.appendChild(new MkDriveWindow().$mount().$el);
 		},
 		settings() {
 			this.close();
diff --git a/src/web/app/desktop/views/components/ui-header-nav.vue b/src/web/app/desktop/views/components/ui-header-nav.vue
index fe0c38778..6d2c3bd47 100644
--- a/src/web/app/desktop/views/components/ui-header-nav.vue
+++ b/src/web/app/desktop/views/components/ui-header-nav.vue
@@ -34,6 +34,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import MkMessagingWindow from './messaging-window.vue';
 
 export default Vue.extend({
 	data() {
@@ -76,8 +77,7 @@ export default Vue.extend({
 		},
 
 		messaging() {
-			// TODO
-			//document.body.appendChild(new MkMessagingWindow().$mount().$el);
+			document.body.appendChild(new MkMessagingWindow().$mount().$el);
 		}
 	}
 });
diff --git a/src/web/app/common/views/components/widgets/calendar.vue b/src/web/app/desktop/views/components/widgets/calendar.vue
similarity index 98%
rename from src/web/app/common/views/components/widgets/calendar.vue
rename to src/web/app/desktop/views/components/widgets/calendar.vue
index 308f43cd9..8574bf59f 100644
--- a/src/web/app/common/views/components/widgets/calendar.vue
+++ b/src/web/app/desktop/views/components/widgets/calendar.vue
@@ -35,7 +35,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../define-widget';
+import define from '../../../../common/define-widget';
 export default define({
 	name: 'calendar',
 	props: {
diff --git a/src/web/app/common/views/components/widgets/donation.vue b/src/web/app/desktop/views/components/widgets/donation.vue
similarity index 94%
rename from src/web/app/common/views/components/widgets/donation.vue
rename to src/web/app/desktop/views/components/widgets/donation.vue
index 50adc531b..b3e0658a4 100644
--- a/src/web/app/common/views/components/widgets/donation.vue
+++ b/src/web/app/desktop/views/components/widgets/donation.vue
@@ -12,7 +12,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../define-widget';
+import define from '../../../../common/define-widget';
 export default define({
 	name: 'donation'
 });
diff --git a/src/web/app/desktop/views/components/widgets/messaging.vue b/src/web/app/desktop/views/components/widgets/messaging.vue
index f31acc5c6..733989b78 100644
--- a/src/web/app/desktop/views/components/widgets/messaging.vue
+++ b/src/web/app/desktop/views/components/widgets/messaging.vue
@@ -6,7 +6,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../define-widget';
+import define from '../../../../common/define-widget';
 export default define({
 	name: 'messaging',
 	props: {
diff --git a/src/web/app/common/views/components/widgets/nav.vue b/src/web/app/desktop/views/components/widgets/nav.vue
similarity index 86%
rename from src/web/app/common/views/components/widgets/nav.vue
rename to src/web/app/desktop/views/components/widgets/nav.vue
index 77e1eea49..a782ad62b 100644
--- a/src/web/app/common/views/components/widgets/nav.vue
+++ b/src/web/app/desktop/views/components/widgets/nav.vue
@@ -5,7 +5,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../define-widget';
+import define from '../../../../common/define-widget';
 export default define({
 	name: 'nav'
 });
diff --git a/src/web/app/desktop/views/components/widgets/notifications.vue b/src/web/app/desktop/views/components/widgets/notifications.vue
new file mode 100644
index 000000000..2d613fa23
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/notifications.vue
@@ -0,0 +1,70 @@
+<template>
+<div class="mkw-notifications">
+	<template v-if="!props.compact">
+		<p class="title">%fa:R bell%%i18n:desktop.tags.mk-notifications-home-widget.title%</p>
+		<button @click="settings" title="%i18n:desktop.tags.mk-notifications-home-widget.settings%">%fa:cog%</button>
+	</template>
+	<mk-notifications/>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+export default define({
+	name: 'notifications',
+	props: {
+		compact: false
+	}
+}).extend({
+	methods: {
+		settings() {
+			alert('not implemented yet');
+		},
+		func() {
+			this.props.compact = !this.props.compact;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-notifications
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	> .title
+		z-index 1
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> [data-fa]
+			margin-right 4px
+
+	> button
+		position absolute
+		z-index 2
+		top 0
+		right 0
+		padding 0
+		width 42px
+		font-size 0.9em
+		line-height 42px
+		color #ccc
+
+		&:hover
+			color #aaa
+
+		&:active
+			color #999
+
+	> .mk-notifications
+		max-height 300px
+		overflow auto
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/photo-stream.vue b/src/web/app/desktop/views/components/widgets/photo-stream.vue
similarity index 97%
rename from src/web/app/common/views/components/widgets/photo-stream.vue
rename to src/web/app/desktop/views/components/widgets/photo-stream.vue
index 4d6b66069..a3f37e8c7 100644
--- a/src/web/app/common/views/components/widgets/photo-stream.vue
+++ b/src/web/app/desktop/views/components/widgets/photo-stream.vue
@@ -10,7 +10,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../define-widget';
+import define from '../../../../common/define-widget';
 export default define({
 	name: 'photo-stream',
 	props: {
diff --git a/src/web/app/common/views/components/widgets/profile.vue b/src/web/app/desktop/views/components/widgets/profile.vue
similarity index 97%
rename from src/web/app/common/views/components/widgets/profile.vue
rename to src/web/app/desktop/views/components/widgets/profile.vue
index d64ffad93..9a0d40a5c 100644
--- a/src/web/app/common/views/components/widgets/profile.vue
+++ b/src/web/app/desktop/views/components/widgets/profile.vue
@@ -21,7 +21,7 @@
 </template>
 
 <script lang="ts">
-import define from '../../../define-widget';
+import define from '../../../../common/define-widget';
 export default define({
 	name: 'profile',
 	props: {
diff --git a/src/web/app/common/views/components/widgets/slideshow.vue b/src/web/app/desktop/views/components/widgets/slideshow.vue
similarity index 98%
rename from src/web/app/common/views/components/widgets/slideshow.vue
rename to src/web/app/desktop/views/components/widgets/slideshow.vue
index ea8e38a2c..beda35066 100644
--- a/src/web/app/common/views/components/widgets/slideshow.vue
+++ b/src/web/app/desktop/views/components/widgets/slideshow.vue
@@ -12,7 +12,7 @@
 
 <script lang="ts">
 import * as anime from 'animejs';
-import define from '../../../define-widget';
+import define from '../../../../common/define-widget';
 export default define({
 	name: 'slideshow',
 	props: {
diff --git a/src/web/app/common/views/components/widgets/tips.vue b/src/web/app/desktop/views/components/widgets/tips.vue
similarity index 98%
rename from src/web/app/common/views/components/widgets/tips.vue
rename to src/web/app/desktop/views/components/widgets/tips.vue
index d9e1fbc94..2991fbc3b 100644
--- a/src/web/app/common/views/components/widgets/tips.vue
+++ b/src/web/app/desktop/views/components/widgets/tips.vue
@@ -6,7 +6,7 @@
 
 <script lang="ts">
 import * as anime from 'animejs';
-import define from '../../../define-widget';
+import define from '../../../../common/define-widget';
 
 const tips = [
 	'<kbd>t</kbd>でタイムラインにフォーカスできます',

From df67bbac9fb7d87b0fda08d118298a85675dd34c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 18 Feb 2018 23:56:25 +0900
Subject: [PATCH 187/286] wip

---
 src/web/app/common/views/components/messaging-form.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/web/app/common/views/components/messaging-form.vue b/src/web/app/common/views/components/messaging-form.vue
index 37ac51509..0b0ab8ade 100644
--- a/src/web/app/common/views/components/messaging-form.vue
+++ b/src/web/app/common/views/components/messaging-form.vue
@@ -33,7 +33,7 @@ export default Vue.extend({
 			const items = data.items;
 			for (const item of items) {
 				if (item.kind == 'file') {
-					this.upload(item.getAsFile());
+					//this.upload(item.getAsFile());
 				}
 			}
 		},
@@ -58,7 +58,7 @@ export default Vue.extend({
 
 		upload() {
 			// TODO
-		}
+		},
 
 		send() {
 			this.sending = true;
@@ -76,7 +76,7 @@ export default Vue.extend({
 
 		clear() {
 			this.text = '';
-			this.files = [];
+			this.file = null;
 		}
 	}
 });

From fed959ae9ae084aca9ca7093c6c79b9ca6507abf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 00:18:01 +0900
Subject: [PATCH 188/286] wip

---
 .../views/components/messaging-form.vue       |  2 +-
 .../views/components/messaging-message.vue    | 19 ++++++++++++-------
 .../views/components/messaging-room.vue       |  4 ++--
 .../desktop/views/components/drive-window.vue |  4 ++--
 4 files changed, 17 insertions(+), 12 deletions(-)

diff --git a/src/web/app/common/views/components/messaging-form.vue b/src/web/app/common/views/components/messaging-form.vue
index 0b0ab8ade..470606b77 100644
--- a/src/web/app/common/views/components/messaging-form.vue
+++ b/src/web/app/common/views/components/messaging-form.vue
@@ -1,5 +1,5 @@
 <template>
-<div>
+<div class="mk-messaging-form">
 	<textarea v-model="text" @keypress="onKeypress" @paste="onPaste" placeholder="%i18n:common.input-message-here%"></textarea>
 	<div class="files"></div>
 	<mk-uploader ref="uploader"/>
diff --git a/src/web/app/common/views/components/messaging-message.vue b/src/web/app/common/views/components/messaging-message.vue
index 6f44332af..d2e3dacb5 100644
--- a/src/web/app/common/views/components/messaging-message.vue
+++ b/src/web/app/common/views/components/messaging-message.vue
@@ -1,23 +1,28 @@
 <template>
 <div class="mk-messaging-message" :data-is-me="isMe">
-	<a class="avatar-anchor" href={ '/' + message.user.username } title={ message.user.username } target="_blank">
-		<img class="avatar" src={ message.user.avatar_url + '?thumbnail&size=80' } alt=""/>
+	<a class="avatar-anchor" :href="`/${message.user.username}`" :title="message.user.username" target="_blank">
+		<img class="avatar" :src="`${message.user.avatar_url}?thumbnail&size=80`" alt=""/>
 	</a>
 	<div class="content-container">
 		<div class="balloon">
 			<p class="read" v-if="message.is_me && message.is_read">%i18n:common.tags.mk-messaging-message.is-read%</p>
-			<button class="delete-button" v-if="message.is_me" title="%i18n:common.delete%"><img src="/assets/desktop/messaging/delete.png" alt="Delete"/></button>
+			<button class="delete-button" v-if="message.is_me" title="%i18n:common.delete%">
+				<img src="/assets/desktop/messaging/delete.png" alt="Delete"/>
+			</button>
 			<div class="content" v-if="!message.is_deleted">
-				<mk-post-html v-if="message.ast" :ast="message.ast" :i="os.i"/>
+				<mk-post-html class="text" v-if="message.ast" :ast="message.ast" :i="os.i"/>
 				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-				<div class="image" v-if="message.file"><img src={ message.file.url } alt="image" title={ message.file.name }/></div>
+				<div class="image" v-if="message.file">
+					<img :src="message.file.url" alt="image" :title="message.file.name"/>
+				</div>
 			</div>
 			<div class="content" v-if="message.is_deleted">
 				<p class="is-deleted">%i18n:common.tags.mk-messaging-message.deleted%</p>
 			</div>
 		</div>
 		<footer>
-			<mk-time time={ message.created_at }/><template v-if="message.is_edited">%fa:pencil-alt%</template>
+			<mk-time :time="message.created_at"/>
+			<template v-if="message.is_edited">%fa:pencil-alt%</template>
 		</footer>
 	</div>
 </div>
@@ -139,7 +144,7 @@ export default Vue.extend({
 					font-size 1em
 					color rgba(0, 0, 0, 0.5)
 
-				> [ref='text']
+				> .text
 					display block
 					margin 0
 					padding 8px 16px
diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index 978610d7f..d03799563 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -3,8 +3,8 @@
 	<div class="stream">
 		<p class="init" v-if="init">%fa:spinner .spin%%i18n:common.loading%</p>
 		<p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:common.tags.mk-messaging-room.empty%</p>
-		<p class="no-history" v-if="!init && messages.length > 0 && !moreMessagesIsInStock">%fa:flag%%i18n:common.tags.mk-messaging-room.no-history%</p>
-		<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="moreMessagesIsInStock" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
+		<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages">%fa:flag%%i18n:common.tags.mk-messaging-room.no-history%</p>
+		<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
 			<template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' }}
 		</button>
 		<template v-for="(message, i) in _messages">
diff --git a/src/web/app/desktop/views/components/drive-window.vue b/src/web/app/desktop/views/components/drive-window.vue
index 309ae14b5..af0fea68d 100644
--- a/src/web/app/desktop/views/components/drive-window.vue
+++ b/src/web/app/desktop/views/components/drive-window.vue
@@ -2,9 +2,9 @@
 <mk-window ref="window" @closed="$destroy" width="800px" height="500px" :popout="popout">
 	<template slot="header">
 		<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:desktop.tags.mk-drive-browser-window.used%</p>
-		<span: class="$style.title">%fa:cloud%%i18n:desktop.tags.mk-drive-browser-window.drive%</span>
+		<span :class="$style.title">%fa:cloud%%i18n:desktop.tags.mk-drive-browser-window.drive%</span>
 	</template>
-	<mk-drive-browser multiple :folder="folder" ref="browser"/>
+	<mk-drive multiple :folder="folder" ref="browser"/>
 </mk-window>
 </template>
 

From c903df34c3ed8faf248576e4b5db0b78aaf6e5ce Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 00:21:18 +0900
Subject: [PATCH 189/286] wip

---
 src/web/app/desktop/views/components/drive-window.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/desktop/views/components/drive-window.vue b/src/web/app/desktop/views/components/drive-window.vue
index af0fea68d..5a6b7c1b5 100644
--- a/src/web/app/desktop/views/components/drive-window.vue
+++ b/src/web/app/desktop/views/components/drive-window.vue
@@ -4,7 +4,7 @@
 		<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:desktop.tags.mk-drive-browser-window.used%</p>
 		<span :class="$style.title">%fa:cloud%%i18n:desktop.tags.mk-drive-browser-window.drive%</span>
 	</template>
-	<mk-drive multiple :folder="folder" ref="browser"/>
+	<mk-drive multiple :init-folder="folder" ref="browser"/>
 </mk-window>
 </template>
 

From 2fe720c61020b56b32b72cf7fb008aeb2d6a5a98 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 00:38:49 +0900
Subject: [PATCH 190/286] wip

---
 .gitattributes | 2 --
 1 file changed, 2 deletions(-)

diff --git a/.gitattributes b/.gitattributes
index c6c5947ba..952d6cd0e 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,5 +1,3 @@
 *.svg -diff -text
 *.psd -diff -text
 *.ai -diff -text
-
-*.tag linguist-language=HTML

From 469ff88287086d91444aafcfa94b1d61e16fe3f9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 14:29:42 +0900
Subject: [PATCH 191/286] wip

---
 src/web/app/common/views/components/nav.vue   |  8 +++++++-
 src/web/app/desktop/script.ts                 |  3 +++
 .../desktop/views/components/posts-post.vue   | 18 ++++++++++--------
 .../views/components/ui-header-nav.vue        |  4 ++--
 .../desktop/views/directives/user-preview.ts  | 19 +++++++++----------
 .../desktop/views/pages/user/user-home.vue    |  2 +-
 src/web/app/desktop/views/pages/user/user.vue | 13 ++++++++-----
 webpack/plugins/index.ts                      |  2 +-
 8 files changed, 41 insertions(+), 28 deletions(-)

diff --git a/src/web/app/common/views/components/nav.vue b/src/web/app/common/views/components/nav.vue
index 6cd86216c..8ce75d352 100644
--- a/src/web/app/common/views/components/nav.vue
+++ b/src/web/app/common/views/components/nav.vue
@@ -1,5 +1,5 @@
 <template>
-<span>
+<span class="mk-nav">
 	<a :href="aboutUrl">%i18n:common.tags.mk-nav-links.about%</a>
 	<i>・</i>
 	<a :href="statsUrl">%i18n:common.tags.mk-nav-links.stats%</a>
@@ -33,3 +33,9 @@ export default Vue.extend({
 	}
 });
 </script>
+
+<style lang="stylus" scoped>
+.mk-nav
+	a
+		color inherit
+</style>
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index cb7a53fb2..7278c9af1 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -16,6 +16,7 @@ import dialog from './api/dialog';
 import input from './api/input';
 
 import MkIndex from './views/pages/index.vue';
+import MkUser from './views/pages/user/user.vue';
 
 /**
  * init
@@ -55,6 +56,8 @@ init(async (launch) => {
 
 	app.$router.addRoutes([{
 		path: '/', component: MkIndex
+	}, {
+		path: '/:user', component: MkUser
 	}]);
 }, true);
 
diff --git a/src/web/app/desktop/views/components/posts-post.vue b/src/web/app/desktop/views/components/posts-post.vue
index 90db8088c..f16811609 100644
--- a/src/web/app/desktop/views/components/posts-post.vue
+++ b/src/web/app/desktop/views/components/posts-post.vue
@@ -5,32 +5,34 @@
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<a class="avatar-anchor" :href="`/${post.user.username}`" v-user-preview="post.user_id">
+			<router-link class="avatar-anchor" :to="`/${post.user.username}`" v-user-preview="post.user_id">
 				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
-			</a>
+			</router-link>
 			%fa:retweet%{{'%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{'))}}<a class="name" :href="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</a>{{'%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1)}}
 		</p>
 		<mk-time :time="post.created_at"/>
 	</div>
 	<article>
-		<a class="avatar-anchor" :href="`/${p.user.username}`">
+		<router-link class="avatar-anchor" :to="`/${p.user.username}`">
 			<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
-		</a>
+		</router-link>
 		<div class="main">
 			<header>
-				<a class="name" :href="`/${p.user.username}`" v-user-preview="p.user.id">{{ p.user.name }}</a>
+				<router-link class="name" :to="`/${p.user.username}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link>
 				<span class="is-bot" v-if="p.user.is_bot">bot</span>
 				<span class="username">@{{ p.user.username }}</span>
 				<div class="info">
 					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
-					<a class="created-at" :href="url">
+					<router-link class="created-at" :to="url">
 						<mk-time :time="p.created_at"/>
-					</a>
+					</router-link>
 				</div>
 			</header>
 			<div class="body">
 				<div class="text" ref="text">
-					<p class="channel" v-if="p.channel"><a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:</p>
+					<p class="channel" v-if="p.channel">
+						<a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:
+					</p>
 					<a class="reply" v-if="p.reply">%fa:reply%</a>
 					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i"/>
 					<a class="quote" v-if="p.repost">RP:</a>
diff --git a/src/web/app/desktop/views/components/ui-header-nav.vue b/src/web/app/desktop/views/components/ui-header-nav.vue
index 6d2c3bd47..cf276dc5c 100644
--- a/src/web/app/desktop/views/components/ui-header-nav.vue
+++ b/src/web/app/desktop/views/components/ui-header-nav.vue
@@ -3,10 +3,10 @@
 	<ul>
 		<template v-if="os.isSignedIn">
 			<li class="home" :class="{ active: page == 'home' }">
-				<a href="/">
+				<router-link to="/">
 					%fa:home%
 					<p>%i18n:desktop.tags.mk-ui-header-nav.home%</p>
-				</a>
+				</router-link>
 			</li>
 			<li class="messaging">
 				<a @click="messaging">
diff --git a/src/web/app/desktop/views/directives/user-preview.ts b/src/web/app/desktop/views/directives/user-preview.ts
index 6e800ee73..8a4035881 100644
--- a/src/web/app/desktop/views/directives/user-preview.ts
+++ b/src/web/app/desktop/views/directives/user-preview.ts
@@ -6,32 +6,31 @@ import MkUserPreview from '../components/user-preview.vue';
 
 export default {
 	bind(el, binding, vn) {
-		const self = vn.context._userPreviewDirective_ = {} as any;
+		const self = el._userPreviewDirective_ = {} as any;
 
 		self.user = binding.value;
-
-		let tag = null;
+		self.tag = null;
 		self.showTimer = null;
 		self.hideTimer = null;
 
 		self.close = () => {
-			if (tag) {
-				tag.close();
-				tag = null;
+			if (self.tag) {
+				self.tag.close();
+				self.tag = null;
 			}
 		};
 
 		const show = () => {
-			if (tag) return;
+			if (self.tag) return;
 
-			tag = new MkUserPreview({
+			self.tag = new MkUserPreview({
 				parent: vn.context,
 				propsData: {
 					user: self.user
 				}
 			}).$mount();
 
-			const preview = tag.$el;
+			const preview = self.tag.$el;
 			const rect = el.getBoundingClientRect();
 			const x = rect.left + el.offsetWidth + window.pageXOffset;
 			const y = rect.top + window.pageYOffset;
@@ -65,7 +64,7 @@ export default {
 	},
 
 	unbind(el, binding, vn) {
-		const self = vn.context._userPreviewDirective_;
+		const self = el._userPreviewDirective_;
 		clearTimeout(self.showTimer);
 		clearTimeout(self.hideTimer);
 		self.close();
diff --git a/src/web/app/desktop/views/pages/user/user-home.vue b/src/web/app/desktop/views/pages/user/user-home.vue
index 2e67b1ec3..ca2c68840 100644
--- a/src/web/app/desktop/views/pages/user/user-home.vue
+++ b/src/web/app/desktop/views/pages/user/user-home.vue
@@ -17,7 +17,7 @@
 			<mk-calendar-widget @warp="warp" :start="new Date(user.created_at)"/>
 			<mk-activity-widget :user="user"/>
 			<mk-user-friends :user="user"/>
-			<div class="nav"><mk-nav-links/></div>
+			<div class="nav"><mk-nav/></div>
 		</div>
 	</div>
 </div>
diff --git a/src/web/app/desktop/views/pages/user/user.vue b/src/web/app/desktop/views/pages/user/user.vue
index 3339c2dce..765057e65 100644
--- a/src/web/app/desktop/views/pages/user/user.vue
+++ b/src/web/app/desktop/views/pages/user/user.vue
@@ -10,13 +10,16 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import Progress from '../../../common/scripts/loading';
+import Progress from '../../../../common/scripts/loading';
+import MkUserHeader from './user-header.vue';
+import MkUserHome from './user-home.vue';
 
 export default Vue.extend({
+	components: {
+		'mk-user-header': MkUserHeader,
+		'mk-user-home': MkUserHome
+	},
 	props: {
-		username: {
-			type: String
-		},
 		page: {
 			default: 'home'
 		}
@@ -30,7 +33,7 @@ export default Vue.extend({
 	mounted() {
 		Progress.start();
 		(this as any).api('users/show', {
-			username: this.username
+			username: this.$route.params.user
 		}).then(user => {
 			this.fetching = false;
 			this.user = user;
diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
index a29d2b7e2..027f60224 100644
--- a/webpack/plugins/index.ts
+++ b/webpack/plugins/index.ts
@@ -9,7 +9,7 @@ const isProduction = env === 'production';
 
 export default (version, lang) => {
 	const plugins = [
-		new HardSourceWebpackPlugin(),
+		//new HardSourceWebpackPlugin(),
 		consts(lang)
 	];
 

From 62152bfa6f5c9ee6ae555033b5f9cf19ab8e2d25 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 15:55:17 +0900
Subject: [PATCH 192/286] wip

---
 .../home-widgets/user-recommendation.tag      |  4 +-
 src/web/app/desktop/views/components/index.ts |  2 +
 .../views/components/post-detail-sub.vue      |  4 +-
 .../desktop/views/components/post-detail.vue  | 61 ++++++++++---------
 .../views/components/widgets/donation.vue     |  2 +-
 .../desktop/views/pages/user/user-friends.vue |  4 +-
 6 files changed, 40 insertions(+), 37 deletions(-)

diff --git a/src/web/app/desktop/-tags/home-widgets/user-recommendation.tag b/src/web/app/desktop/-tags/home-widgets/user-recommendation.tag
index bc873539e..b2a19d71f 100644
--- a/src/web/app/desktop/-tags/home-widgets/user-recommendation.tag
+++ b/src/web/app/desktop/-tags/home-widgets/user-recommendation.tag
@@ -5,10 +5,10 @@
 	</template>
 	<div class="user" v-if="!loading && users.length != 0" each={ _user in users }>
 		<a class="avatar-anchor" href={ '/' + _user.username }>
-			<img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ _user.id }/>
+			<img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" v-user-preview={ _user.id }/>
 		</a>
 		<div class="body">
-			<a class="name" href={ '/' + _user.username } data-user-preview={ _user.id }>{ _user.name }</a>
+			<a class="name" href={ '/' + _user.username } v-user-preview={ _user.id }>{ _user.name }</a>
 			<p class="username">@{ _user.username }</p>
 		</div>
 		<mk-follow-button user={ _user }/>
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 4b390ffdd..b8d167f22 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -32,6 +32,7 @@ import driveFile from './drive-file.vue';
 import driveFolder from './drive-folder.vue';
 import driveNavFolder from './drive-nav-folder.vue';
 import contextMenuMenu from './context-menu-menu.vue';
+import postDetail from './post-detail.vue';
 import wNav from './widgets/nav.vue';
 import wCalendar from './widgets/calendar.vue';
 import wPhotoStream from './widgets/photo-stream.vue';
@@ -72,6 +73,7 @@ Vue.component('mk-drive-file', driveFile);
 Vue.component('mk-drive-folder', driveFolder);
 Vue.component('mk-drive-nav-folder', driveNavFolder);
 Vue.component('context-menu-menu', contextMenuMenu);
+Vue.component('post-detail', postDetail);
 Vue.component('mkw-nav', wNav);
 Vue.component('mkw-calendar', wCalendar);
 Vue.component('mkw-photo-stream', wPhotoStream);
diff --git a/src/web/app/desktop/views/components/post-detail-sub.vue b/src/web/app/desktop/views/components/post-detail-sub.vue
index 44ed5edd8..320720dfb 100644
--- a/src/web/app/desktop/views/components/post-detail-sub.vue
+++ b/src/web/app/desktop/views/components/post-detail-sub.vue
@@ -1,12 +1,12 @@
 <template>
 <div class="mk-post-detail-sub" :title="title">
 	<a class="avatar-anchor" href={ '/' + post.user.username }>
-		<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar" data-user-preview={ post.user_id }/>
+		<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar" v-user-preview={ post.user_id }/>
 	</a>
 	<div class="main">
 		<header>
 			<div class="left">
-				<a class="name" href={ '/' + post.user.username } data-user-preview={ post.user_id }>{ post.user.name }</a>
+				<a class="name" href={ '/' + post.user.username } v-user-preview={ post.user_id }>{ post.user.name }</a>
 				<span class="username">@{ post.user.username }</span>
 			</div>
 			<div class="right">
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index dd4a32b6e..c2c2559f6 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -1,57 +1,60 @@
 <template>
 <div class="mk-post-detail" :title="title">
-	<button class="read-more" v-if="p.reply && p.reply.reply_id && context == null" title="会話をもっと読み込む" @click="loadContext" disabled={ contextFetching }>
+	<button
+		class="read-more"
+		v-if="p.reply && p.reply.reply_id && context == null"
+		title="会話をもっと読み込む"
+		@click="loadContext"
+		:disabled="contextFetching"
+	>
 		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
 		<template v-if="contextFetching">%fa:spinner .pulse%</template>
 	</button>
 	<div class="context">
-		<template each={ post in context }>
-			<mk-post-detail-sub post={ post }/>
-		</template>
+		<mk-post-detail-sub v-for="post in context" :key="post.id" :post="post"/>
 	</div>
 	<div class="reply-to" v-if="p.reply">
-		<mk-post-detail-sub post={ p.reply }/>
+		<mk-post-detail-sub :post="p.reply"/>
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<a class="avatar-anchor" href={ '/' + post.user.username } data-user-preview={ post.user_id }>
-				<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=32' } alt="avatar"/>
-			</a>
-			%fa:retweet%<a class="name" href={ '/' + post.user.username }>
-			{ post.user.name }
-		</a>
-		がRepost
-	</p>
+			<router-link class="avatar-anchor" :to="`/${post.user.username}`" v-user-preview="post.user_id">
+				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%
+			<router-link class="name" :href="`/${post.user.username}`">{{ post.user.name }}</router-link>
+			がRepost
+		</p>
 	</div>
 	<article>
-		<a class="avatar-anchor" href={ '/' + p.user.username }>
-			<img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=64' } alt="avatar" data-user-preview={ p.user.id }/>
-		</a>
+		<router-link class="avatar-anchor" :to="`/${p.user.username}`">
+			<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
+		</router-link>
 		<header>
-			<a class="name" href={ '/' + p.user.username } data-user-preview={ p.user.id }>{ p.user.name }</a>
-			<span class="username">@{ p.user.username }</span>
-			<a class="time" href={ '/' + p.user.username + '/' + p.id }>
-				<mk-time time={ p.created_at }/>
-			</a>
+			<router-link class="name" :to="`/${p.user.username}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link>
+			<span class="username">@{{ p.user.username }}</span>
+			<router-link class="time" :to="`/${p.user.username}/${p.id}`">
+				<mk-time :time="p.created_at"/>
+			</router-link>
 		</header>
 		<div class="body">
 			<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i"/>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			<div class="media" v-if="p.media">
-				<mk-images images={ p.media }/>
+				<mk-images :images="p.media"/>
 			</div>
-			<mk-poll v-if="p.poll" post={ p }/>
+			<mk-poll v-if="p.poll" :post="p"/>
 		</div>
 		<footer>
-			<mk-reactions-viewer post={ p }/>
+			<mk-reactions-viewer :post="p"/>
 			<button @click="reply" title="返信">
-				%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
+				%fa:reply%<p class="count" v-if="p.replies_count > 0">{{ p.replies_count }}</p>
 			</button>
 			<button @click="repost" title="Repost">
-				%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
+				%fa:retweet%<p class="count" v-if="p.repost_count > 0">{{ p.repost_count }}</p>
 			</button>
 			<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton" title="リアクション">
-				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
+				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
 			</button>
 			<button @click="menu" ref="menuButton">
 				%fa:ellipsis-h%
@@ -59,9 +62,7 @@
 		</footer>
 	</article>
 	<div class="replies" v-if="!compact">
-		<template each={ post in replies }>
-			<mk-post-detail-sub post={ post }/>
-		</template>
+		<mk-post-detail-sub v-for="post in nreplies" :key="post.id" :post="post"/>
 	</div>
 </div>
 </template>
diff --git a/src/web/app/desktop/views/components/widgets/donation.vue b/src/web/app/desktop/views/components/widgets/donation.vue
index b3e0658a4..8eb1706c6 100644
--- a/src/web/app/desktop/views/components/widgets/donation.vue
+++ b/src/web/app/desktop/views/components/widgets/donation.vue
@@ -4,7 +4,7 @@
 		<h1>%fa:heart%%i18n:desktop.tags.mk-donation-home-widget.title%</h1>
 		<p>
 			{{ '%i18n:desktop.tags.mk-donation-home-widget.text%'.substr(0, '%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('{')) }}
-			<a href="/syuilo" data-user-preview="@syuilo">@syuilo</a>
+			<a href="/syuilo" v-user-preview="@syuilo">@syuilo</a>
 			{{ '%i18n:desktop.tags.mk-donation-home-widget.text%'.substr('%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('}') + 1) }}
 		</p>
 	</article>
diff --git a/src/web/app/desktop/views/pages/user/user-friends.vue b/src/web/app/desktop/views/pages/user/user-friends.vue
index d6b20aa27..9f324cfc0 100644
--- a/src/web/app/desktop/views/pages/user/user-friends.vue
+++ b/src/web/app/desktop/views/pages/user/user-friends.vue
@@ -4,10 +4,10 @@
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p>
 	<div class="user" v-if="!fetching && users.length != 0" each={ _user in users }>
 		<a class="avatar-anchor" href={ '/' + _user.username }>
-			<img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" data-user-preview={ _user.id }/>
+			<img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" v-user-preview={ _user.id }/>
 		</a>
 		<div class="body">
-			<a class="name" href={ '/' + _user.username } data-user-preview={ _user.id }>{ _user.name }</a>
+			<a class="name" href={ '/' + _user.username } v-user-preview={ _user.id }>{ _user.name }</a>
 			<p class="username">@{ _user.username }</p>
 		</div>
 		<mk-follow-button user={ _user }/>

From 363cd2a66bdf8d5eeac3da569ea3398d0494d212 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 16:08:41 +0900
Subject: [PATCH 193/286] wip

---
 .../desktop/-tags/home-widgets/broadcast.tag  | 143 ----------------
 src/web/app/desktop/views/components/index.ts |   2 +
 .../views/components/widgets/broadcast.vue    | 153 ++++++++++++++++++
 .../views/components/widgets/donation.vue     |   2 +-
 .../desktop/views/components/widgets/nav.vue  |   2 +-
 5 files changed, 157 insertions(+), 145 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/broadcast.tag
 create mode 100644 src/web/app/desktop/views/components/widgets/broadcast.vue

diff --git a/src/web/app/desktop/-tags/home-widgets/broadcast.tag b/src/web/app/desktop/-tags/home-widgets/broadcast.tag
deleted file mode 100644
index 91ddbb4ab..000000000
--- a/src/web/app/desktop/-tags/home-widgets/broadcast.tag
+++ /dev/null
@@ -1,143 +0,0 @@
-<mk-broadcast-home-widget data-found={ broadcasts.length != 0 } data-melt={ data.design == 1 }>
-	<div class="icon">
-		<svg height="32" version="1.1" viewBox="0 0 32 32" width="32">
-			<path class="tower" d="M16.04,11.24c1.79,0,3.239-1.45,3.239-3.24S17.83,4.76,16.04,4.76c-1.79,0-3.24,1.45-3.24,3.24 C12.78,9.78,14.24,11.24,16.04,11.24z M16.04,13.84c-0.82,0-1.66-0.2-2.4-0.6L7.34,29.98h2.98l1.72-2h8l1.681,2H24.7L18.42,13.24 C17.66,13.64,16.859,13.84,16.04,13.84z M16.02,14.8l2.02,7.2h-4L16.02,14.8z M12.04,25.98l2-2h4l2,2H12.04z"></path>
-			<path class="wave a" d="M4.66,1.04c-0.508-0.508-1.332-0.508-1.84,0c-1.86,1.92-2.8,4.44-2.8,6.94c0,2.52,0.94,5.04,2.8,6.96 c0.5,0.52,1.32,0.52,1.82,0s0.5-1.36,0-1.88C3.28,11.66,2.6,9.82,2.6,7.98S3.28,4.3,4.64,2.9C5.157,2.391,5.166,1.56,4.66,1.04z"></path>
-			<path class="wave b" d="M9.58,12.22c0.5-0.5,0.5-1.34,0-1.84C8.94,9.72,8.62,8.86,8.62,8s0.32-1.72,0.96-2.38c0.5-0.52,0.5-1.34,0-1.84 C9.346,3.534,9.02,3.396,8.68,3.4c-0.32,0-0.66,0.12-0.9,0.38C6.64,4.94,6.08,6.48,6.08,8s0.58,3.06,1.7,4.22 C8.28,12.72,9.1,12.72,9.58,12.22z"></path>
-			<path class="wave c" d="M22.42,3.78c-0.5,0.5-0.5,1.34,0,1.84c0.641,0.66,0.96,1.52,0.96,2.38s-0.319,1.72-0.96,2.38c-0.5,0.52-0.5,1.34,0,1.84 c0.487,0.497,1.285,0.505,1.781,0.018c0.007-0.006,0.013-0.012,0.02-0.018c1.139-1.16,1.699-2.7,1.699-4.22s-0.561-3.06-1.699-4.22 c-0.494-0.497-1.297-0.5-1.794-0.007C22.424,3.775,22.422,3.778,22.42,3.78z"></path>
-			<path class="wave d" d="M29.18,1.06c-0.479-0.502-1.273-0.522-1.775-0.044c-0.016,0.015-0.029,0.029-0.045,0.044c-0.5,0.52-0.5,1.36,0,1.88 c1.361,1.4,2.041,3.24,2.041,5.08s-0.68,3.66-2.041,5.08c-0.5,0.52-0.5,1.36,0,1.88c0.509,0.508,1.332,0.508,1.841,0 c1.86-1.92,2.8-4.44,2.8-6.96C31.99,5.424,30.98,2.931,29.18,1.06z"></path>
-		</svg>
-	</div>
-	<p class="fetching" v-if="fetching">%i18n:desktop.tags.mk-broadcast-home-widget.fetching%<mk-ellipsis/></p>
-	<h1 v-if="!fetching">{
-		broadcasts.length == 0 ? '%i18n:desktop.tags.mk-broadcast-home-widget.no-broadcasts%' : broadcasts[i].title
-	}</h1>
-	<p v-if="!fetching"><mk-raw v-if="broadcasts.length != 0" content={ broadcasts[i].text }/><template v-if="broadcasts.length == 0">%i18n:desktop.tags.mk-broadcast-home-widget.have-a-nice-day%</template></p>
-	<a v-if="broadcasts.length > 1" @click="next">%i18n:desktop.tags.mk-broadcast-home-widget.next% &gt;&gt;</a>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding 10px
-			border solid 1px #4078c0
-			border-radius 6px
-
-			&[data-melt]
-				border none
-
-			&[data-found]
-				padding-left 50px
-
-				> .icon
-					display block
-
-			&:after
-				content ""
-				display block
-				clear both
-
-			> .icon
-				display none
-				float left
-				margin-left -40px
-
-				> svg
-					fill currentColor
-					color #4078c0
-
-					> .wave
-						opacity 1
-
-						&.a
-							animation wave 20s ease-in-out 2.1s infinite
-						&.b
-							animation wave 20s ease-in-out 2s infinite
-						&.c
-							animation wave 20s ease-in-out 2s infinite
-						&.d
-							animation wave 20s ease-in-out 2.1s infinite
-
-						@keyframes wave
-							0%
-								opacity 1
-							1.5%
-								opacity 0
-							3.5%
-								opacity 0
-							5%
-								opacity 1
-							6.5%
-								opacity 0
-							8.5%
-								opacity 0
-							10%
-								opacity 1
-
-			> h1
-				margin 0
-				font-size 0.95em
-				font-weight normal
-				color #4078c0
-
-			> p
-				display block
-				z-index 1
-				margin 0
-				font-size 0.7em
-				color #555
-
-				&.fetching
-					text-align center
-
-				a
-					color #555
-					text-decoration underline
-
-			> a
-				display block
-				font-size 0.7em
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			design: 0
-		};
-
-		this.mixin('widget');
-		this.mixin('os');
-
-		this.i = 0;
-		this.fetching = true;
-		this.broadcasts = [];
-
-		this.on('mount', () => {
-			this.mios.getMeta().then(meta => {
-				let broadcasts = [];
-				if (meta.broadcasts) {
-					meta.broadcasts.forEach(broadcast => {
-						if (broadcast[_LANG_]) {
-							broadcasts.push(broadcast[_LANG_]);
-						}
-					});
-				}
-				this.update({
-					fetching: false,
-					broadcasts: broadcasts
-				});
-			});
-		});
-
-		this.next = () => {
-			if (this.i == this.broadcasts.length - 1) {
-				this.i = 0;
-			} else {
-				this.i++;
-			}
-			this.update();
-		};
-
-		this.func = () => {
-			if (++this.data.design == 2) this.data.design = 0;
-			this.save();
-		};
-	</script>
-</mk-broadcast-home-widget>
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index b8d167f22..1f28613d2 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -40,6 +40,7 @@ import wSlideshow from './widgets/slideshow.vue';
 import wTips from './widgets/tips.vue';
 import wDonation from './widgets/donation.vue';
 import wNotifications from './widgets/notifications.vue';
+import wBroadcast from './widgets/broadcast.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-header', uiHeader);
@@ -81,3 +82,4 @@ Vue.component('mkw-slideshoe', wSlideshow);
 Vue.component('mkw-tips', wTips);
 Vue.component('mkw-donation', wDonation);
 Vue.component('mkw-notifications', wNotifications);
+Vue.component('mkw-broadcast', wBroadcast);
diff --git a/src/web/app/desktop/views/components/widgets/broadcast.vue b/src/web/app/desktop/views/components/widgets/broadcast.vue
new file mode 100644
index 000000000..cdc65a2a7
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/broadcast.vue
@@ -0,0 +1,153 @@
+<template>
+<div class="mkw-broadcast" :data-found="broadcasts.length != 0" :data-melt="props.design == 1">
+	<div class="icon">
+		<svg height="32" version="1.1" viewBox="0 0 32 32" width="32">
+			<path class="tower" d="M16.04,11.24c1.79,0,3.239-1.45,3.239-3.24S17.83,4.76,16.04,4.76c-1.79,0-3.24,1.45-3.24,3.24 C12.78,9.78,14.24,11.24,16.04,11.24z M16.04,13.84c-0.82,0-1.66-0.2-2.4-0.6L7.34,29.98h2.98l1.72-2h8l1.681,2H24.7L18.42,13.24 C17.66,13.64,16.859,13.84,16.04,13.84z M16.02,14.8l2.02,7.2h-4L16.02,14.8z M12.04,25.98l2-2h4l2,2H12.04z"></path>
+			<path class="wave a" d="M4.66,1.04c-0.508-0.508-1.332-0.508-1.84,0c-1.86,1.92-2.8,4.44-2.8,6.94c0,2.52,0.94,5.04,2.8,6.96 c0.5,0.52,1.32,0.52,1.82,0s0.5-1.36,0-1.88C3.28,11.66,2.6,9.82,2.6,7.98S3.28,4.3,4.64,2.9C5.157,2.391,5.166,1.56,4.66,1.04z"></path>
+			<path class="wave b" d="M9.58,12.22c0.5-0.5,0.5-1.34,0-1.84C8.94,9.72,8.62,8.86,8.62,8s0.32-1.72,0.96-2.38c0.5-0.52,0.5-1.34,0-1.84 C9.346,3.534,9.02,3.396,8.68,3.4c-0.32,0-0.66,0.12-0.9,0.38C6.64,4.94,6.08,6.48,6.08,8s0.58,3.06,1.7,4.22 C8.28,12.72,9.1,12.72,9.58,12.22z"></path>
+			<path class="wave c" d="M22.42,3.78c-0.5,0.5-0.5,1.34,0,1.84c0.641,0.66,0.96,1.52,0.96,2.38s-0.319,1.72-0.96,2.38c-0.5,0.52-0.5,1.34,0,1.84 c0.487,0.497,1.285,0.505,1.781,0.018c0.007-0.006,0.013-0.012,0.02-0.018c1.139-1.16,1.699-2.7,1.699-4.22s-0.561-3.06-1.699-4.22 c-0.494-0.497-1.297-0.5-1.794-0.007C22.424,3.775,22.422,3.778,22.42,3.78z"></path>
+			<path class="wave d" d="M29.18,1.06c-0.479-0.502-1.273-0.522-1.775-0.044c-0.016,0.015-0.029,0.029-0.045,0.044c-0.5,0.52-0.5,1.36,0,1.88 c1.361,1.4,2.041,3.24,2.041,5.08s-0.68,3.66-2.041,5.08c-0.5,0.52-0.5,1.36,0,1.88c0.509,0.508,1.332,0.508,1.841,0 c1.86-1.92,2.8-4.44,2.8-6.96C31.99,5.424,30.98,2.931,29.18,1.06z"></path>
+		</svg>
+	</div>
+	<p class="fetching" v-if="fetching">%i18n:desktop.tags.mk-broadcast-home-widget.fetching%<mk-ellipsis/></p>
+	<h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:desktop.tags.mk-broadcast-home-widget.no-broadcasts%' : broadcasts[i].title }}</h1>
+	<p v-if="!fetching">
+		<span v-if="broadcasts.length != 0" :v-html="broadcasts[i].text"></span>
+		<template v-if="broadcasts.length == 0">%i18n:desktop.tags.mk-broadcast-home-widget.have-a-nice-day%</template>
+	</p>
+	<a v-if="broadcasts.length > 1" @click="next">%i18n:desktop.tags.mk-broadcast-home-widget.next% &gt;&gt;</a>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+import { lang } from '../../../../config';
+
+export default define({
+	name: 'broadcast',
+	props: {
+		design: 0
+	}
+}).extend({
+	data() {
+		return {
+			i: 0,
+			fetching: true,
+			broadcasts: []
+		};
+	},
+	mounted() {
+		(this as any).os.getMeta().then(meta => {
+			let broadcasts = [];
+			if (meta.broadcasts) {
+				meta.broadcasts.forEach(broadcast => {
+					if (broadcast[lang]) {
+						broadcasts.push(broadcast[lang]);
+					}
+				});
+			}
+			this.fetching = false;
+			this.broadcasts = broadcasts;
+		});
+	},
+	methods: {
+		next() {
+			if (this.i == this.broadcasts.length - 1) {
+				this.i = 0;
+			} else {
+				this.i++;
+			}
+		},
+		func() {
+			if (this.props.design == 1) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-broadcast
+	padding 10px
+	border solid 1px #4078c0
+	border-radius 6px
+
+	&[data-melt]
+		border none
+
+	&[data-found]
+		padding-left 50px
+
+		> .icon
+			display block
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	> .icon
+		display none
+		float left
+		margin-left -40px
+
+		> svg
+			fill currentColor
+			color #4078c0
+
+			> .wave
+				opacity 1
+
+				&.a
+					animation wave 20s ease-in-out 2.1s infinite
+				&.b
+					animation wave 20s ease-in-out 2s infinite
+				&.c
+					animation wave 20s ease-in-out 2s infinite
+				&.d
+					animation wave 20s ease-in-out 2.1s infinite
+
+				@keyframes wave
+					0%
+						opacity 1
+					1.5%
+						opacity 0
+					3.5%
+						opacity 0
+					5%
+						opacity 1
+					6.5%
+						opacity 0
+					8.5%
+						opacity 0
+					10%
+						opacity 1
+
+	> h1
+		margin 0
+		font-size 0.95em
+		font-weight normal
+		color #4078c0
+
+	> p
+		display block
+		z-index 1
+		margin 0
+		font-size 0.7em
+		color #555
+
+		&.fetching
+			text-align center
+
+		a
+			color #555
+			text-decoration underline
+
+	> a
+		display block
+		font-size 0.7em
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/donation.vue b/src/web/app/desktop/views/components/widgets/donation.vue
index 8eb1706c6..fbab0fca6 100644
--- a/src/web/app/desktop/views/components/widgets/donation.vue
+++ b/src/web/app/desktop/views/components/widgets/donation.vue
@@ -4,7 +4,7 @@
 		<h1>%fa:heart%%i18n:desktop.tags.mk-donation-home-widget.title%</h1>
 		<p>
 			{{ '%i18n:desktop.tags.mk-donation-home-widget.text%'.substr(0, '%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('{')) }}
-			<a href="/syuilo" v-user-preview="@syuilo">@syuilo</a>
+			<a href="https://syuilo.com">@syuilo</a>
 			{{ '%i18n:desktop.tags.mk-donation-home-widget.text%'.substr('%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('}') + 1) }}
 		</p>
 	</article>
diff --git a/src/web/app/desktop/views/components/widgets/nav.vue b/src/web/app/desktop/views/components/widgets/nav.vue
index a782ad62b..5e04c266c 100644
--- a/src/web/app/desktop/views/components/widgets/nav.vue
+++ b/src/web/app/desktop/views/components/widgets/nav.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mkw-nav">
-	<mk-nav-links/>
+	<mk-nav/>
 </div>
 </template>
 

From 818e421ce2cc789f6762297772e6a240a7e3a99b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 16:18:18 +0900
Subject: [PATCH 194/286] wip

---
 src/web/app/desktop/views/components/context-menu.vue    | 6 +++++-
 src/web/app/desktop/views/components/index.ts            | 6 +++---
 src/web/app/desktop/views/components/settings-window.vue | 4 +---
 3 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/src/web/app/desktop/views/components/context-menu.vue b/src/web/app/desktop/views/components/context-menu.vue
index 9238b4246..3ba475e11 100644
--- a/src/web/app/desktop/views/components/context-menu.vue
+++ b/src/web/app/desktop/views/components/context-menu.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="context-menu" :style="{ left: `${x}px`, top: `${y}px` }" @contextmenu.prevent="() => {}">
-	<context-menu-menu :menu="menu" @x="click"/>
+	<me-nu :menu="menu" @x="click"/>
 </div>
 </template>
 
@@ -8,8 +8,12 @@
 import Vue from 'vue';
 import * as anime from 'animejs';
 import contains from '../../../common/scripts/contains';
+import meNu from './context-menu-menu.vue';
 
 export default Vue.extend({
+	components: {
+		'me-nu': meNu
+	},
 	props: ['x', 'y', 'menu'],
 	mounted() {
 		this.$nextTick(() => {
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 1f28613d2..151ebf296 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -31,8 +31,8 @@ import drive from './drive.vue';
 import driveFile from './drive-file.vue';
 import driveFolder from './drive-folder.vue';
 import driveNavFolder from './drive-nav-folder.vue';
-import contextMenuMenu from './context-menu-menu.vue';
 import postDetail from './post-detail.vue';
+import settings from './settings.vue';
 import wNav from './widgets/nav.vue';
 import wCalendar from './widgets/calendar.vue';
 import wPhotoStream from './widgets/photo-stream.vue';
@@ -73,8 +73,8 @@ Vue.component('mk-drive', drive);
 Vue.component('mk-drive-file', driveFile);
 Vue.component('mk-drive-folder', driveFolder);
 Vue.component('mk-drive-nav-folder', driveNavFolder);
-Vue.component('context-menu-menu', contextMenuMenu);
-Vue.component('post-detail', postDetail);
+Vue.component('mk-post-detail', postDetail);
+Vue.component('mk-settings', settings);
 Vue.component('mkw-nav', wNav);
 Vue.component('mkw-calendar', wCalendar);
 Vue.component('mkw-photo-stream', wPhotoStream);
diff --git a/src/web/app/desktop/views/components/settings-window.vue b/src/web/app/desktop/views/components/settings-window.vue
index 9b264da0f..c4e1d6a0a 100644
--- a/src/web/app/desktop/views/components/settings-window.vue
+++ b/src/web/app/desktop/views/components/settings-window.vue
@@ -1,9 +1,7 @@
 <template>
 <mk-window is-modal width='700px' height='550px' @closed="$destroy">
 	<span slot="header" :class="$style.header">%fa:cog%設定</span>
-	<div slot="content">
-		<mk-settings/>
-	</div>
+	<mk-settings/>
 </mk-window>
 </template>
 

From 3e7b980fb683f69fc5524eb4fcb10f5945290916 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 16:58:37 +0900
Subject: [PATCH 195/286] wip

---
 .../app/desktop/-tags/widgets/calendar.tag    | 241 -----------------
 .../app/desktop/views/components/calendar.vue | 251 ++++++++++++++++++
 .../views/components/profile-setting.vue      |  10 +-
 .../app/desktop/views/components/settings.vue |   5 +
 4 files changed, 263 insertions(+), 244 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/widgets/calendar.tag
 create mode 100644 src/web/app/desktop/views/components/calendar.vue

diff --git a/src/web/app/desktop/-tags/widgets/calendar.tag b/src/web/app/desktop/-tags/widgets/calendar.tag
deleted file mode 100644
index d20180f1c..000000000
--- a/src/web/app/desktop/-tags/widgets/calendar.tag
+++ /dev/null
@@ -1,241 +0,0 @@
-<mk-calendar-widget data-melt={ opts.design == 4 || opts.design == 5 }>
-	<template v-if="opts.design == 0 || opts.design == 1">
-		<button @click="prev" title="%i18n:desktop.tags.mk-calendar-widget.prev%">%fa:chevron-circle-left%</button>
-		<p class="title">{ '%i18n:desktop.tags.mk-calendar-widget.title%'.replace('{1}', year).replace('{2}', month) }</p>
-		<button @click="next" title="%i18n:desktop.tags.mk-calendar-widget.next%">%fa:chevron-circle-right%</button>
-	</template>
-
-	<div class="calendar">
-		<div class="weekday" v-if="opts.design == 0 || opts.design == 2 || opts.design == 4} each={ day, i in Array(7).fill(0)"
-			data-today={ year == today.getFullYear() && month == today.getMonth() + 1 && today.getDay() == i }
-			data-is-donichi={ i == 0 || i == 6 }>{ weekdayText[i] }</div>
-		<div each={ day, i in Array(paddingDays).fill(0) }></div>
-		<div class="day" each={ day, i in Array(days).fill(0) }
-				data-today={ isToday(i + 1) }
-				data-selected={ isSelected(i + 1) }
-				data-is-out-of-range={ isOutOfRange(i + 1) }
-				data-is-donichi={ isDonichi(i + 1) }
-				@click="go.bind(null, i + 1)"
-				title={ isOutOfRange(i + 1) ? null : '%i18n:desktop.tags.mk-calendar-widget.go%' }><div>{ i + 1 }</div></div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			color #777
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			&[data-melt]
-				background transparent !important
-				border none !important
-
-			> .title
-				z-index 1
-				margin 0
-				padding 0 16px
-				text-align center
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> [data-fa]
-					margin-right 4px
-
-			> button
-				position absolute
-				z-index 2
-				top 0
-				padding 0
-				width 42px
-				font-size 0.9em
-				line-height 42px
-				color #ccc
-
-				&:hover
-					color #aaa
-
-				&:active
-					color #999
-
-				&:first-of-type
-					left 0
-
-				&:last-of-type
-					right 0
-
-			> .calendar
-				display flex
-				flex-wrap wrap
-				padding 16px
-
-				*
-					user-select none
-
-				> div
-					width calc(100% * (1/7))
-					text-align center
-					line-height 32px
-					font-size 14px
-
-					&.weekday
-						color #19a2a9
-
-						&[data-is-donichi]
-							color #ef95a0
-
-						&[data-today]
-							box-shadow 0 0 0 1px #19a2a9 inset
-							border-radius 6px
-
-							&[data-is-donichi]
-								box-shadow 0 0 0 1px #ef95a0 inset
-
-					&.day
-						cursor pointer
-						color #777
-
-						> div
-							border-radius 6px
-
-						&:hover > div
-							background rgba(0, 0, 0, 0.025)
-
-						&:active > div
-							background rgba(0, 0, 0, 0.05)
-
-						&[data-is-donichi]
-							color #ef95a0
-
-						&[data-is-out-of-range]
-							cursor default
-							color rgba(#777, 0.5)
-
-							&[data-is-donichi]
-								color rgba(#ef95a0, 0.5)
-
-						&[data-selected]
-							font-weight bold
-
-							> div
-								background rgba(0, 0, 0, 0.025)
-
-							&:active > div
-								background rgba(0, 0, 0, 0.05)
-
-						&[data-today]
-							> div
-								color $theme-color-foreground
-								background $theme-color
-
-							&:hover > div
-								background lighten($theme-color, 10%)
-
-							&:active > div
-								background darken($theme-color, 10%)
-
-	</style>
-	<script lang="typescript">
-		if (this.opts.design == null) this.opts.design = 0;
-
-		const eachMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
-
-		function isLeapYear(year) {
-			return (year % 400 == 0) ? true :
-				(year % 100 == 0) ? false :
-					(year % 4 == 0) ? true :
-						false;
-		}
-
-		this.today = new Date();
-		this.year = this.today.getFullYear();
-		this.month = this.today.getMonth() + 1;
-		this.selected = this.today;
-		this.weekdayText = [
-			'%i18n:common.weekday-short.sunday%',
-			'%i18n:common.weekday-short.monday%',
-			'%i18n:common.weekday-short.tuesday%',
-			'%i18n:common.weekday-short.wednesday%',
-			'%i18n:common.weekday-short.thursday%',
-			'%i18n:common.weekday-short.friday%',
-			'%i18n:common.weekday-short.satruday%'
-		];
-
-		this.on('mount', () => {
-			this.calc();
-		});
-
-		this.isToday = day => {
-			return this.year == this.today.getFullYear() && this.month == this.today.getMonth() + 1 && day == this.today.getDate();
-		};
-
-		this.isSelected = day => {
-			return this.year == this.selected.getFullYear() && this.month == this.selected.getMonth() + 1 && day == this.selected.getDate();
-		};
-
-		this.isOutOfRange = day => {
-			const test = (new Date(this.year, this.month - 1, day)).getTime();
-			return test > this.today.getTime() ||
-				(this.opts.start ? test < this.opts.start.getTime() : false);
-		};
-
-		this.isDonichi = day => {
-			const weekday = (new Date(this.year, this.month - 1, day)).getDay();
-			return weekday == 0 || weekday == 6;
-		};
-
-		this.calc = () => {
-			let days = eachMonthDays[this.month - 1];
-
-			// うるう年なら+1日
-			if (this.month == 2 && isLeapYear(this.year)) days++;
-
-			const date = new Date(this.year, this.month - 1, 1);
-			const weekday = date.getDay();
-
-			this.update({
-				paddingDays: weekday,
-				days: days
-			});
-		};
-
-		this.prev = () => {
-			if (this.month == 1) {
-				this.update({
-					year: this.year - 1,
-					month: 12
-				});
-			} else {
-				this.update({
-					month: this.month - 1
-				});
-			}
-			this.calc();
-		};
-
-		this.next = () => {
-			if (this.month == 12) {
-				this.update({
-					year: this.year + 1,
-					month: 1
-				});
-			} else {
-				this.update({
-					month: this.month + 1
-				});
-			}
-			this.calc();
-		};
-
-		this.go = day => {
-			if (this.isOutOfRange(day)) return;
-			const date = new Date(this.year, this.month - 1, day, 23, 59, 59, 999);
-			this.update({
-				selected: date
-			});
-			this.opts.warp(date);
-		};
-</script>
-</mk-calendar-widget>
diff --git a/src/web/app/desktop/views/components/calendar.vue b/src/web/app/desktop/views/components/calendar.vue
new file mode 100644
index 000000000..e55411929
--- /dev/null
+++ b/src/web/app/desktop/views/components/calendar.vue
@@ -0,0 +1,251 @@
+<template>
+<div class="mk-calendar">
+	<template v-if="design == 0 || design == 1">
+		<button @click="prev" title="%i18n:desktop.tags.mk-calendar-widget.prev%">%fa:chevron-circle-left%</button>
+		<p class="title">{{ '%i18n:desktop.tags.mk-calendar-widget.title%'.replace('{1}', year).replace('{2}', month) }}</p>
+		<button @click="next" title="%i18n:desktop.tags.mk-calendar-widget.next%">%fa:chevron-circle-right%</button>
+	</template>
+
+	<div class="calendar">
+		<div class="weekday"
+			v-if="design == 0 || design == 2 || design == 4"
+			v-for="(day, i) in Array(7).fill(0)"
+			:key="i"
+			:data-today="year == today.getFullYear() && month == today.getMonth() + 1 && today.getDay() == i"
+			:data-is-donichi="i == 0 || i == 6"
+		>{{ weekdayText[i] }}</div>
+		<div each={ day, i in Array(paddingDays).fill(0) }></div>
+		<div class="day" v-for="(day, i) in Array(days).fill(0)"
+			:key="i"
+			:data-today="isToday(i + 1)"
+			:data-selected="isSelected(i + 1)"
+			:data-is-out-of-range="isOutOfRange(i + 1)"
+			:data-is-donichi="isDonichi(i + 1)"
+			@click="go(i + 1)"
+			:title="isOutOfRange(i + 1) ? null : '%i18n:desktop.tags.mk-calendar-widget.go%'"
+		>
+			<div>{{ i + 1 }}</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+const eachMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+
+function isLeapYear(year) {
+	return (year % 400 == 0) ? true :
+		(year % 100 == 0) ? false :
+			(year % 4 == 0) ? true :
+				false;
+}
+
+export default Vue.extend({
+	props: {
+		design: {
+			default: 0
+		},
+		start: {
+			type: Object,
+			required: false
+		}
+	},
+	data() {
+		return {
+			today: new Date(),
+			year: new Date().getFullYear(),
+			month: new Date().getMonth() + 1,
+			selected: new Date(),
+			weekdayText: [
+				'%i18n:common.weekday-short.sunday%',
+				'%i18n:common.weekday-short.monday%',
+				'%i18n:common.weekday-short.tuesday%',
+				'%i18n:common.weekday-short.wednesday%',
+				'%i18n:common.weekday-short.thursday%',
+				'%i18n:common.weekday-short.friday%',
+				'%i18n:common.weekday-short.satruday%'
+			]
+		};
+	},
+	computed: {
+		paddingDays(): number {
+			const date = new Date(this.year, this.month - 1, 1);
+			return date.getDay();
+		},
+		days(): number {
+			let days = eachMonthDays[this.month - 1];
+
+			// うるう年なら+1日
+			if (this.month == 2 && isLeapYear(this.year)) days++;
+
+			return days;
+		}
+	},
+	methods: {
+		isToday(day) {
+			return this.year == this.today.getFullYear() && this.month == this.today.getMonth() + 1 && day == this.today.getDate();
+		},
+
+		isSelected(day) {
+			return this.year == this.selected.getFullYear() && this.month == this.selected.getMonth() + 1 && day == this.selected.getDate();
+		},
+
+		isOutOfRange(day) {
+			const test = (new Date(this.year, this.month - 1, day)).getTime();
+			return test > this.today.getTime() ||
+				(this.start ? test < this.start.getTime() : false);
+		},
+
+		isDonichi(day) {
+			const weekday = (new Date(this.year, this.month - 1, day)).getDay();
+			return weekday == 0 || weekday == 6;
+		},
+
+		prev() {
+			if (this.month == 1) {
+				this.year = this.year - 1;
+				this.month = 12;
+			} else {
+				this.month--;
+			}
+		},
+
+		next() {
+			if (this.month == 12) {
+				this.year = this.year + 1;
+				this.month = 1;
+			} else {
+				this.month++;
+			}
+		},
+
+		go(day) {
+			if (this.isOutOfRange(day)) return;
+			const date = new Date(this.year, this.month - 1, day, 23, 59, 59, 999);
+			this.selected = date;
+			this.$emit('choosed', this.selected);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-calendar
+	color #777
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	&[data-melt]
+		background transparent !important
+		border none !important
+
+	> .title
+		z-index 1
+		margin 0
+		padding 0 16px
+		text-align center
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> [data-fa]
+			margin-right 4px
+
+	> button
+		position absolute
+		z-index 2
+		top 0
+		padding 0
+		width 42px
+		font-size 0.9em
+		line-height 42px
+		color #ccc
+
+		&:hover
+			color #aaa
+
+		&:active
+			color #999
+
+		&:first-of-type
+			left 0
+
+		&:last-of-type
+			right 0
+
+	> .calendar
+		display flex
+		flex-wrap wrap
+		padding 16px
+
+		*
+			user-select none
+
+		> div
+			width calc(100% * (1/7))
+			text-align center
+			line-height 32px
+			font-size 14px
+
+			&.weekday
+				color #19a2a9
+
+				&[data-is-donichi]
+					color #ef95a0
+
+				&[data-today]
+					box-shadow 0 0 0 1px #19a2a9 inset
+					border-radius 6px
+
+					&[data-is-donichi]
+						box-shadow 0 0 0 1px #ef95a0 inset
+
+			&.day
+				cursor pointer
+				color #777
+
+				> div
+					border-radius 6px
+
+				&:hover > div
+					background rgba(0, 0, 0, 0.025)
+
+				&:active > div
+					background rgba(0, 0, 0, 0.05)
+
+				&[data-is-donichi]
+					color #ef95a0
+
+				&[data-is-out-of-range]
+					cursor default
+					color rgba(#777, 0.5)
+
+					&[data-is-donichi]
+						color rgba(#ef95a0, 0.5)
+
+				&[data-selected]
+					font-weight bold
+
+					> div
+						background rgba(0, 0, 0, 0.025)
+
+					&:active > div
+						background rgba(0, 0, 0, 0.05)
+
+				&[data-today]
+					> div
+						color $theme-color-foreground
+						background $theme-color
+
+					&:hover > div
+						background lighten($theme-color, 10%)
+
+					&:active > div
+						background darken($theme-color, 10%)
+
+</style>
diff --git a/src/web/app/desktop/views/components/profile-setting.vue b/src/web/app/desktop/views/components/profile-setting.vue
index 403488ef1..b61de33ef 100644
--- a/src/web/app/desktop/views/components/profile-setting.vue
+++ b/src/web/app/desktop/views/components/profile-setting.vue
@@ -1,7 +1,8 @@
 <template>
 <div class="mk-profile-setting">
 	<label class="avatar ui from group">
-		<p>%i18n:desktop.tags.mk-profile-setting.avatar%</p><img class="avatar" :src="`${os.i.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+		<p>%i18n:desktop.tags.mk-profile-setting.avatar%</p>
+		<img class="avatar" :src="`${os.i.avatar_url}?thumbnail&size=64`" alt="avatar"/>
 		<button class="ui" @click="updateAvatar">%i18n:desktop.tags.mk-profile-setting.choice-avatar%</button>
 	</label>
 	<label class="ui from group">
@@ -26,7 +27,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import updateAvatar from '../../scripts/update-avatar';
 import notify from '../../scripts/notify';
 
 export default Vue.extend({
@@ -40,7 +40,11 @@ export default Vue.extend({
 	},
 	methods: {
 		updateAvatar() {
-			updateAvatar((this as any).os.i);
+			(this as any).apis.chooseDriveFile({
+				multiple: false
+			}).then(file => {
+				(this as any).apis.updateAvatar(file);
+			});
 		},
 		save() {
 			(this as any).api('i/update', {
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index fe996689a..e9a9bbfa8 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -73,7 +73,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import MkProfileSetting from './profile-setting.vue';
+
 export default Vue.extend({
+	components: {
+		'mk-profie-setting': MkProfileSetting
+	},
 	data() {
 		return {
 			page: 'profile'

From 4705c935ab2554fd791817a288b395520200d284 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 17:03:22 +0900
Subject: [PATCH 196/286] wip

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

diff --git a/src/web/app/desktop/views/components/calendar.vue b/src/web/app/desktop/views/components/calendar.vue
index e55411929..338077402 100644
--- a/src/web/app/desktop/views/components/calendar.vue
+++ b/src/web/app/desktop/views/components/calendar.vue
@@ -125,7 +125,7 @@ export default Vue.extend({
 			if (this.isOutOfRange(day)) return;
 			const date = new Date(this.year, this.month - 1, day, 23, 59, 59, 999);
 			this.selected = date;
-			this.$emit('choosed', this.selected);
+			this.$emit('chosen', this.selected);
 		}
 	}
 });

From 0c9a7cf6434a5238a1b97870e04c74b75e6191f5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 18:26:20 +0900
Subject: [PATCH 197/286] wip

---
 .eslintrc                                     |  17 ++
 package.json                                  |   3 +
 .../-tags/home-widgets/timemachine.tag        |  23 --
 .../app/desktop/views/components/calendar.vue |   9 +-
 src/web/app/desktop/views/components/home.vue | 224 ++++++++----------
 src/web/app/desktop/views/components/index.ts |   4 +
 .../app/desktop/views/components/timeline.vue |  15 +-
 .../views/components/widgets/timemachine.vue  |  28 +++
 8 files changed, 161 insertions(+), 162 deletions(-)
 create mode 100644 .eslintrc
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/timemachine.tag
 create mode 100644 src/web/app/desktop/views/components/widgets/timemachine.vue

diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 000000000..d30cf2aa5
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,17 @@
+{
+	"parserOptions": {
+		"parser": "typescript-eslint-parser"
+	},
+	"extends": [
+		"eslint:recommended",
+		"plugin:vue/recommended"
+	],
+	"rules": {
+		"vue/require-v-for-key": false,
+		"vue/max-attributes-per-line": false,
+		"vue/html-indent": false,
+		"vue/html-self-closing": false,
+		"vue/no-unused-vars": false,
+		"no-console": 0
+	}
+}
diff --git a/package.json b/package.json
index e87be0ab2..727c4af71 100644
--- a/package.json
+++ b/package.json
@@ -99,6 +99,8 @@
 		"diskusage": "0.2.4",
 		"elasticsearch": "14.1.0",
 		"escape-regexp": "0.0.1",
+		"eslint": "^4.18.0",
+		"eslint-plugin-vue": "^4.2.2",
 		"eventemitter3": "3.0.0",
 		"exif-js": "2.3.0",
 		"express": "4.16.2",
@@ -174,6 +176,7 @@
 		"ts-node": "4.1.0",
 		"tslint": "5.9.1",
 		"typescript": "2.7.1",
+		"typescript-eslint-parser": "^13.0.0",
 		"uglify-es": "3.3.9",
 		"uglifyjs-webpack-plugin": "1.1.8",
 		"uuid": "3.2.1",
diff --git a/src/web/app/desktop/-tags/home-widgets/timemachine.tag b/src/web/app/desktop/-tags/home-widgets/timemachine.tag
deleted file mode 100644
index 43f59fe67..000000000
--- a/src/web/app/desktop/-tags/home-widgets/timemachine.tag
+++ /dev/null
@@ -1,23 +0,0 @@
-<mk-timemachine-home-widget>
-	<mk-calendar-widget design={ data.design } warp={ warp }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		this.data = {
-			design: 0
-		};
-
-		this.mixin('widget');
-
-		this.warp = date => {
-			this.opts.tl.warp(date);
-		};
-
-		this.func = () => {
-			if (++this.data.design == 6) this.data.design = 0;
-			this.save();
-		};
-	</script>
-</mk-timemachine-home-widget>
diff --git a/src/web/app/desktop/views/components/calendar.vue b/src/web/app/desktop/views/components/calendar.vue
index 338077402..e548a82c5 100644
--- a/src/web/app/desktop/views/components/calendar.vue
+++ b/src/web/app/desktop/views/components/calendar.vue
@@ -7,16 +7,15 @@
 	</template>
 
 	<div class="calendar">
+		<template v-if="design == 0 || design == 2 || design == 4">
 		<div class="weekday"
-			v-if="design == 0 || design == 2 || design == 4"
 			v-for="(day, i) in Array(7).fill(0)"
-			:key="i"
 			:data-today="year == today.getFullYear() && month == today.getMonth() + 1 && today.getDay() == i"
 			:data-is-donichi="i == 0 || i == 6"
 		>{{ weekdayText[i] }}</div>
-		<div each={ day, i in Array(paddingDays).fill(0) }></div>
-		<div class="day" v-for="(day, i) in Array(days).fill(0)"
-			:key="i"
+		</template>
+		<div v-for="n in paddingDays"></div>
+		<div class="day" v-for="(day, i) in days"
 			:data-today="isToday(i + 1)"
 			:data-selected="isSelected(i + 1)"
 			:data-is-out-of-range="isOutOfRange(i + 1)"
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 3a04e13cb..e815239d3 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -37,43 +37,21 @@
 		</div>
 	</div>
 	<div class="main">
-		<div class="left">
-			<div ref="left" data-place="left">
-				<template v-for="widget in leftWidgets">
+		<div v-for="place in ['left', 'main', 'right']" :class="place" :ref="place" :data-place="place">
+			<template v-if="place != 'main'">
+				<template v-for="widget in widgets[place]">
 					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
-						<component :is="'mkw-' + widget.name" :widget="widget" :ref="widget.id"/>
+						<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id"/>
 					</div>
 					<template v-else>
-						<component :is="'mkw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
+						<component :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :ref="widget.id" @chosen="warp"/>
 					</template>
 				</template>
-			</div>
-		</div>
-		<main ref="main">
-			<div class="maintop" ref="maintop" data-place="main" v-if="customize">
-				<template v-for="widget in centerWidgets">
-					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
-						<component :is="'mkw-' + widget.name" :widget="widget" :ref="widget.id"/>
-					</div>
-					<template v-else>
-						<component :is="'mkw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
-					</template>
-				</template>
-			</div>
-			<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
-			<mk-mentions ref="tl" @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
-		</main>
-		<div class="right">
-			<div ref="right" data-place="right">
-				<template v-for="widget in rightWidgets">
-					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
-						<component :is="'mkw-' + widget.name" :widget="widget" :ref="widget.id"/>
-					</div>
-					<template v-else>
-						<component :is="'mkw-' + widget.name" :key="widget.id" :widget="widget" :ref="widget.id"/>
-					</template>
-				</template>
-			</div>
+			</template>
+			<template v-else>
+				<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="place == 'main' && mode == 'timeline'"/>
+				<mk-mentions @loaded="onTlLoaded" v-if="place == 'main' && mode == 'mentions'"/>
+			</template>
 		</div>
 	</div>
 </div>
@@ -99,6 +77,85 @@ export default Vue.extend({
 			widgetAdderSelected: null
 		};
 	},
+	computed: {
+		leftWidgets(): any {
+			return (this as any).os.i.client_settings.home.filter(w => w.place == 'left');
+		},
+		rightWidgets(): any {
+			return (this as any).os.i.client_settings.home.filter(w => w.place == 'right');
+		},
+		widgets(): any {
+			return {
+				left: this.leftWidgets,
+				right: this.rightWidgets,
+			};
+		},
+		leftEl(): Element {
+			return (this.$refs.left as Element[])[0];
+		},
+		rightEl(): Element {
+			return (this.$refs.right as Element[])[0];
+		}
+	},
+	created() {
+		this.bakedHomeData = this.bakeHomeData();
+	},
+	mounted() {
+		(this as any).os.i.on('refreshed', this.onMeRefreshed);
+
+		this.home = (this as any).os.i.client_settings.home;
+
+		this.$nextTick(() => {
+			if (!this.customize) {
+				if (this.leftEl.children.length == 0) {
+					this.leftEl.parentNode.removeChild(this.leftEl);
+				}
+				if (this.rightEl.children.length == 0) {
+					this.rightEl.parentNode.removeChild(this.rightEl);
+				}
+			}
+
+			if (this.customize) {
+				(this as any).apis.dialog({
+					title: '%fa:info-circle%カスタマイズのヒント',
+					text: '<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' +
+						'<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' +
+						'<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' +
+						'<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>',
+					actions: [{
+						text: 'Got it!'
+					}]
+				});
+
+				const sortableOption = {
+					group: 'kyoppie',
+					animation: 150,
+					onMove: evt => {
+						const id = evt.dragged.getAttribute('data-widget-id');
+						this.home.find(tag => tag.id == id).widget.place = evt.to.getAttribute('data-place');
+					},
+					onSort: () => {
+						this.saveHome();
+					}
+				};
+
+				new Sortable(this.leftEl, sortableOption);
+				new Sortable(this.rightEl, sortableOption);
+				new Sortable(this.$refs.trash, Object.assign({}, sortableOption, {
+					onAdd: evt => {
+						const el = evt.item;
+						const id = el.getAttribute('data-widget-id');
+						el.parentNode.removeChild(el);
+						(this as any).os.i.client_settings.home = (this as any).os.i.client_settings.home.filter(w => w.id != id);
+						this.saveHome();
+					}
+				}));
+			}
+		});
+	},
+	beforeDestroy() {
+		(this as any).os.i.off('refreshed', this.onMeRefreshed);
+	},
 	methods: {
 		bakeHomeData() {
 			return JSON.stringify((this as any).os.i.client_settings.home);
@@ -130,102 +187,27 @@ export default Vue.extend({
 		saveHome() {
 			const data = [];
 
-			Array.from((this.$refs.left as Element).children).forEach(el => {
+			Array.from(this.leftEl.children).forEach(el => {
 				const id = el.getAttribute('data-widget-id');
 				const widget = (this as any).os.i.client_settings.home.find(w => w.id == id);
 				widget.place = 'left';
 				data.push(widget);
 			});
 
-			Array.from((this.$refs.right as Element).children).forEach(el => {
+			Array.from(this.rightEl.children).forEach(el => {
 				const id = el.getAttribute('data-widget-id');
 				const widget = (this as any).os.i.client_settings.home.find(w => w.id == id);
 				widget.place = 'right';
 				data.push(widget);
 			});
 
-			Array.from((this.$refs.maintop as Element).children).forEach(el => {
-				const id = el.getAttribute('data-widget-id');
-				const widget = (this as any).os.i.client_settings.home.find(w => w.id == id);
-				widget.place = 'main';
-				data.push(widget);
-			});
-
 			(this as any).api('i/update_home', {
 				home: data
 			});
-		}
-	},
-	computed: {
-		leftWidgets(): any {
-			return (this as any).os.i.client_settings.home.filter(w => w.place == 'left');
 		},
-		centerWidgets(): any {
-			return (this as any).os.i.client_settings.home.filter(w => w.place == 'center');
-		},
-		rightWidgets(): any {
-			return (this as any).os.i.client_settings.home.filter(w => w.place == 'right');
+		warp(date) {
+			(this.$refs.tl as any)[0].warp(date);
 		}
-	},
-	created() {
-		this.bakedHomeData = this.bakeHomeData();
-	},
-	mounted() {
-		(this as any).os.i.on('refreshed', this.onMeRefreshed);
-
-		this.home = (this as any).os.i.client_settings.home;
-
-		if (!this.customize) {
-			if ((this.$refs.left as Element).children.length == 0) {
-				(this.$refs.left as Element).parentNode.removeChild((this.$refs.left as Element));
-			}
-			if ((this.$refs.right as Element).children.length == 0) {
-				(this.$refs.right as Element).parentNode.removeChild((this.$refs.right as Element));
-			}
-		}
-
-		if (this.customize) {
-			/*dialog('%fa:info-circle%カスタマイズのヒント',
-				'<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' +
-				'<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' +
-				'<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' +
-				'<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>',
-			[{
-				text: 'Got it!'
-			}]);*/
-
-			const sortableOption = {
-				group: 'kyoppie',
-				animation: 150,
-				onMove: evt => {
-					const id = evt.dragged.getAttribute('data-widget-id');
-					this.home.find(tag => tag.id == id).update({ place: evt.to.getAttribute('data-place') });
-				},
-				onSort: () => {
-					this.saveHome();
-				}
-			};
-
-			new Sortable(this.$refs.left, sortableOption);
-			new Sortable(this.$refs.right, sortableOption);
-			new Sortable(this.$refs.maintop, sortableOption);
-			new Sortable(this.$refs.trash, Object.assign({}, sortableOption, {
-				onAdd: evt => {
-					const el = evt.item;
-					const id = el.getAttribute('data-widget-id');
-					el.parentNode.removeChild(el);
-					(this as any).os.i.client_settings.home = (this as any).os.i.client_settings.home.filter(w => w.id != id);
-					this.saveHome();
-				}
-			}));
-		}
-	},
-	beforeDestroy() {
-		(this as any).os.i.off('refreshed', this.onMeRefreshed);
-
-		this.home.forEach(widget => {
-			widget.unmount();
-		});
 	}
 });
 </script>
@@ -324,26 +306,16 @@ export default Vue.extend({
 				> *
 					pointer-events none
 
-		> main
+		> .main
 			padding 16px
 			width calc(100% - 275px * 2)
 
-			> *:not(.maintop):not(:last-child)
-			> .maintop > *:not(:last-child)
-				margin-bottom 16px
-
-			> .maintop
-				min-height 64px
-				margin-bottom 16px
-
 		> *:not(main)
 			width 275px
+			padding 16px 0 16px 0
 
-			> *
-				padding 16px 0 16px 0
-
-				> *:not(:last-child)
-					margin-bottom 16px
+			> *:not(:last-child)
+				margin-bottom 16px
 
 		> .left
 			padding-left 16px
@@ -355,7 +327,7 @@ export default Vue.extend({
 			> *:not(main)
 				display none
 
-			> main
+			> .main
 				float none
 				width 100%
 				max-width 700px
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 151ebf296..9a2736954 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -33,6 +33,7 @@ import driveFolder from './drive-folder.vue';
 import driveNavFolder from './drive-nav-folder.vue';
 import postDetail from './post-detail.vue';
 import settings from './settings.vue';
+import calendar from './calendar.vue';
 import wNav from './widgets/nav.vue';
 import wCalendar from './widgets/calendar.vue';
 import wPhotoStream from './widgets/photo-stream.vue';
@@ -41,6 +42,7 @@ import wTips from './widgets/tips.vue';
 import wDonation from './widgets/donation.vue';
 import wNotifications from './widgets/notifications.vue';
 import wBroadcast from './widgets/broadcast.vue';
+import wTimemachine from './widgets/timemachine.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-header', uiHeader);
@@ -75,6 +77,7 @@ Vue.component('mk-drive-folder', driveFolder);
 Vue.component('mk-drive-nav-folder', driveNavFolder);
 Vue.component('mk-post-detail', postDetail);
 Vue.component('mk-settings', settings);
+Vue.component('mk-calendar', calendar);
 Vue.component('mkw-nav', wNav);
 Vue.component('mkw-calendar', wCalendar);
 Vue.component('mkw-photo-stream', wPhotoStream);
@@ -83,3 +86,4 @@ Vue.component('mkw-tips', wTips);
 Vue.component('mkw-donation', wDonation);
 Vue.component('mkw-notifications', wNotifications);
 Vue.component('mkw-broadcast', wBroadcast);
+Vue.component('mkw-timemachine', wTimemachine);
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index 3d792436e..66d70a957 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -13,19 +13,14 @@
 import Vue from 'vue';
 
 export default Vue.extend({
-	props: {
-		date: {
-			type: Date,
-			required: false
-		}
-	},
 	data() {
 		return {
 			fetching: true,
 			moreFetching: false,
 			posts: [],
 			connection: null,
-			connectionId: null
+			connectionId: null,
+			date: null
 		};
 	},
 	computed: {
@@ -60,7 +55,7 @@ export default Vue.extend({
 			this.fetching = true;
 
 			(this as any).api('posts/timeline', {
-				until_date: this.date ? (this.date as any).getTime() : undefined
+				until_date: this.date ? this.date.getTime() : undefined
 			}).then(posts => {
 				this.fetching = false;
 				this.posts = posts;
@@ -93,6 +88,10 @@ export default Vue.extend({
 					(this.$refs.timeline as any).focus();
 				}
 			}
+		},
+		warp(date) {
+			this.date = date;
+			this.fetch();
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/widgets/timemachine.vue b/src/web/app/desktop/views/components/widgets/timemachine.vue
new file mode 100644
index 000000000..d484ce6d7
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/timemachine.vue
@@ -0,0 +1,28 @@
+<template>
+<div class="mkw-timemachine">
+	<mk-calendar :design="props.design" @chosen="chosen"/>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+export default define({
+	name: 'timemachine',
+	props: {
+		design: 0
+	}
+}).extend({
+	methods: {
+		chosen(date) {
+			this.$emit('chosen', date);
+		},
+		func() {
+			if (this.props.design == 5) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		}
+	}
+});
+</script>

From 55273807d2b4bd1a7c8ee20883b2ab18e3ec1c9c Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 19:46:31 +0900
Subject: [PATCH 198/286] wip

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

diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index 66d70a957..c63801338 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-timeline">
-	<mk-following-setuper v-if="alone"/>
+	<mk-friends-maker v-if="alone"/>
 	<div class="loading" v-if="fetching">
 		<mk-ellipsis-icon/>
 	</div>

From 69a8e4f4b20a12321eac94d5f8db119ca268cb3c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 19 Feb 2018 23:37:09 +0900
Subject: [PATCH 199/286] wip

---
 src/web/app/common/-tags/authorized-apps.tag  |   1 -
 .../app/common/views/components/messaging.vue |   2 +-
 .../app/desktop/-tags/widgets/activity.tag    | 246 ------------------
 .../views/components/activity.calendar.vue    |  66 +++++
 .../views/components/activity.chart.vue       | 101 +++++++
 .../app/desktop/views/components/activity.vue | 116 +++++++++
 .../app/desktop/views/components/calendar.vue |   4 +-
 .../views/components/followers-window.vue     |   4 +-
 .../views/components/following-window.vue     |   4 +-
 .../views/components/friends-maker.vue        |   2 +-
 src/web/app/desktop/views/components/index.ts |   2 +
 .../desktop/views/components/mute-setting.vue |   2 +-
 .../desktop/views/components/post-detail.vue  |   2 +-
 .../app/desktop/views/components/timeline.vue |   2 +-
 .../desktop/views/components/users-list.vue   |   2 +-
 .../views/components/widgets/broadcast.vue    |   2 +-
 .../views/components/widgets/photo-stream.vue |   2 +-
 .../views/components/widgets/slideshow.vue    |   2 +-
 .../desktop/views/pages/messaging-room.vue    |   2 +-
 src/web/app/desktop/views/pages/post.vue      |   2 +-
 src/web/app/desktop/views/pages/search.vue    |   2 +-
 .../pages/user/user-followers-you-know.vue    |   2 +-
 .../desktop/views/pages/user/user-friends.vue |  22 +-
 .../desktop/views/pages/user/user-home.vue    |  17 +-
 .../desktop/views/pages/user/user-photos.vue  |   5 +-
 .../desktop/views/pages/user/user-profile.vue |  16 +-
 .../user}/user-timeline.vue                   |   2 +-
 src/web/app/desktop/views/pages/user/user.vue |   2 +-
 src/web/app/mobile/views/components/drive.vue |   3 +-
 .../mobile/views/components/friends-maker.vue |   2 +-
 .../app/mobile/views/components/timeline.vue  |   2 +-
 .../mobile/views/components/user-timeline.vue |   2 +-
 .../mobile/views/components/users-list.vue    |   2 +-
 src/web/app/mobile/views/pages/followers.vue  |   2 +-
 src/web/app/mobile/views/pages/following.vue  |   2 +-
 src/web/app/mobile/views/pages/post.vue       |   2 +-
 src/web/app/mobile/views/pages/user.vue       |   2 +-
 .../mobile/views/pages/user/home-friends.vue  |   2 +-
 .../mobile/views/pages/user/home-photos.vue   |   2 +-
 .../mobile/views/pages/user/home-posts.vue    |   2 +-
 40 files changed, 356 insertions(+), 303 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/widgets/activity.tag
 create mode 100644 src/web/app/desktop/views/components/activity.calendar.vue
 create mode 100644 src/web/app/desktop/views/components/activity.chart.vue
 create mode 100644 src/web/app/desktop/views/components/activity.vue
 rename src/web/app/desktop/views/{components => pages/user}/user-timeline.vue (100%)

diff --git a/src/web/app/common/-tags/authorized-apps.tag b/src/web/app/common/-tags/authorized-apps.tag
index 288c2fcc2..ed1570650 100644
--- a/src/web/app/common/-tags/authorized-apps.tag
+++ b/src/web/app/common/-tags/authorized-apps.tag
@@ -28,7 +28,6 @@
 			this.$root.$data.os.api('i/authorized_apps').then(apps => {
 				this.apps = apps;
 				this.fetching = false;
-				this.update();
 			});
 		});
 	</script>
diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index c0b3a1924..c1d541894 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -78,8 +78,8 @@ export default Vue.extend({
 		this.connection.on('read', this.onRead);
 
 		(this as any).api('messaging/history').then(messages => {
-			this.fetching = false;
 			this.messages = messages;
+			this.fetching = false;
 		});
 	},
 	beforeDestroy() {
diff --git a/src/web/app/desktop/-tags/widgets/activity.tag b/src/web/app/desktop/-tags/widgets/activity.tag
deleted file mode 100644
index 1f9bee5ed..000000000
--- a/src/web/app/desktop/-tags/widgets/activity.tag
+++ /dev/null
@@ -1,246 +0,0 @@
-<mk-activity-widget data-melt={ design == 2 }>
-	<template v-if="design == 0">
-		<p class="title">%fa:chart-bar%%i18n:desktop.tags.mk-activity-widget.title%</p>
-		<button @click="toggle" title="%i18n:desktop.tags.mk-activity-widget.toggle%">%fa:sort%</button>
-	</template>
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<mk-activity-widget-calender v-if="!initializing && view == 0" data={ [].concat(activity) }/>
-	<mk-activity-widget-chart v-if="!initializing && view == 1" data={ [].concat(activity) }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			&[data-melt]
-				background transparent !important
-				border none !important
-
-			> .title
-				z-index 1
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> [data-fa]
-					margin-right 4px
-
-			> button
-				position absolute
-				z-index 2
-				top 0
-				right 0
-				padding 0
-				width 42px
-				font-size 0.9em
-				line-height 42px
-				color #ccc
-
-				&:hover
-					color #aaa
-
-				&:active
-					color #999
-
-			> .initializing
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.design = this.opts.design || 0;
-		this.view = this.opts.view || 0;
-
-		this.user = this.opts.user;
-		this.initializing = true;
-
-		this.on('mount', () => {
-			this.$root.$data.os.api('aggregation/users/activity', {
-				user_id: this.user.id,
-				limit: 20 * 7
-			}).then(activity => {
-				this.update({
-					initializing: false,
-					activity
-				});
-			});
-		});
-
-		this.toggle = () => {
-			this.view++;
-			if (this.view == 2) this.view = 0;
-			this.update();
-			this.$emit('view-changed', this.view);
-		};
-	</script>
-</mk-activity-widget>
-
-<mk-activity-widget-calender>
-	<svg viewBox="0 0 21 7" preserveAspectRatio="none">
-		<rect each={ data } class="day"
-			width="1" height="1"
-			riot-x={ x } riot-y={ date.weekday }
-			rx="1" ry="1"
-			fill="transparent">
-			<title>{ date.year }/{ date.month }/{ date.day }<br/>Post: { posts }, Reply: { replies }, Repost: { reposts }</title>
-		</rect>
-		<rect each={ data }
-			riot-width={ v } riot-height={ v }
-			riot-x={ x + ((1 - v) / 2) } riot-y={ date.weekday + ((1 - v) / 2) }
-			rx="1" ry="1"
-			fill={ color }
-			style="pointer-events: none;"/>
-		<rect class="today"
-			width="1" height="1"
-			riot-x={ data[data.length - 1].x } riot-y={ data[data.length - 1].date.weekday }
-			rx="1" ry="1"
-			fill="none"
-			stroke-width="0.1"
-			stroke="#f73520"/>
-	</svg>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> svg
-				display block
-				padding 10px
-				width 100%
-
-				> rect
-					transform-origin center
-
-					&.day
-						&:hover
-							fill rgba(0, 0, 0, 0.05)
-
-	</style>
-	<script lang="typescript">
-		this.data = this.opts.data;
-		this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
-		const peak = Math.max.apply(null, this.data.map(d => d.total));
-
-		let x = 0;
-		this.data.reverse().forEach(d => {
-			d.x = x;
-			d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay();
-
-			d.v = d.total / (peak / 2);
-			if (d.v > 1) d.v = 1;
-			const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
-			const cs = d.v * 100;
-			const cl = 15 + ((1 - d.v) * 80);
-			d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
-
-			if (d.date.weekday == 6) x++;
-		});
-	</script>
-</mk-activity-widget-calender>
-
-<mk-activity-widget-chart>
-	<svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none" onmousedown={ onMousedown }>
-		<title>Black ... Total<br/>Blue ... Posts<br/>Red ... Replies<br/>Green ... Reposts</title>
-		<polyline
-			riot-points={ pointsPost }
-			fill="none"
-			stroke-width="1"
-			stroke="#41ddde"/>
-		<polyline
-			riot-points={ pointsReply }
-			fill="none"
-			stroke-width="1"
-			stroke="#f7796c"/>
-		<polyline
-			riot-points={ pointsRepost }
-			fill="none"
-			stroke-width="1"
-			stroke="#a1de41"/>
-		<polyline
-			riot-points={ pointsTotal }
-			fill="none"
-			stroke-width="1"
-			stroke="#555"
-			stroke-dasharray="2 2"/>
-	</svg>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> svg
-				display block
-				padding 10px
-				width 100%
-				cursor all-scroll
-	</style>
-	<script lang="typescript">
-		this.viewBoxX = 140;
-		this.viewBoxY = 60;
-		this.zoom = 1;
-		this.pos = 0;
-
-		this.data = this.opts.data.reverse();
-		this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
-		const peak = Math.max.apply(null, this.data.map(d => d.total));
-
-		this.on('mount', () => {
-			this.render();
-		});
-
-		this.render = () => {
-			this.update({
-				pointsPost: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' '),
-				pointsReply: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '),
-				pointsRepost: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' '),
-				pointsTotal: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ')
-			});
-		};
-
-		this.onMousedown = e => {
-			e.preventDefault();
-
-			const clickX = e.clientX;
-			const clickY = e.clientY;
-			const baseZoom = this.zoom;
-			const basePos = this.pos;
-
-			// 動かした時
-			dragListen(me => {
-				let moveLeft = me.clientX - clickX;
-				let moveTop = me.clientY - clickY;
-
-				this.zoom = baseZoom + (-moveTop / 20);
-				this.pos = basePos + moveLeft;
-				if (this.zoom < 1) this.zoom = 1;
-				if (this.pos > 0) this.pos = 0;
-				if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX);
-
-				this.render();
-			});
-		};
-
-		function dragListen(fn) {
-			window.addEventListener('mousemove',  fn);
-			window.addEventListener('mouseleave', dragClear.bind(null, fn));
-			window.addEventListener('mouseup',    dragClear.bind(null, fn));
-		}
-
-		function dragClear(fn) {
-			window.removeEventListener('mousemove',  fn);
-			window.removeEventListener('mouseleave', dragClear);
-			window.removeEventListener('mouseup',    dragClear);
-		}
-	</script>
-</mk-activity-widget-chart>
-
diff --git a/src/web/app/desktop/views/components/activity.calendar.vue b/src/web/app/desktop/views/components/activity.calendar.vue
new file mode 100644
index 000000000..d9b852315
--- /dev/null
+++ b/src/web/app/desktop/views/components/activity.calendar.vue
@@ -0,0 +1,66 @@
+<template>
+<svg viewBox="0 0 21 7" preserveAspectRatio="none">
+	<rect v-for="record in data" class="day"
+		width="1" height="1"
+		:x="record.x" :y="record.date.weekday"
+		rx="1" ry="1"
+		fill="transparent">
+		<title>{{ record.date.year }}/{{ record.date.month }}/{{ record.date.day }}</title>
+	</rect>
+	<rect v-for="record in data" class="day"
+		:width="record.v" :height="record.v"
+		:x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)"
+		rx="1" ry="1"
+		:fill="record.color"
+		style="pointer-events: none;"/>
+	<rect class="today"
+		width="1" height="1"
+		:x="data[data.length - 1].x" :y="data[data.length - 1].date.weekday"
+		rx="1" ry="1"
+		fill="none"
+		stroke-width="0.1"
+		stroke="#f73520"/>
+</svg>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['data'],
+	created() {
+		this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
+		const peak = Math.max.apply(null, this.data.map(d => d.total));
+
+		let x = 0;
+		this.data.reverse().forEach(d => {
+			d.x = x;
+			d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay();
+
+			d.v = d.total / (peak / 2);
+			if (d.v > 1) d.v = 1;
+			const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
+			const cs = d.v * 100;
+			const cl = 15 + ((1 - d.v) * 80);
+			d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
+
+			if (d.date.weekday == 6) x++;
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+svg
+	display block
+	padding 10px
+	width 100%
+
+	> rect
+		transform-origin center
+
+		&.day
+			&:hover
+				fill rgba(0, 0, 0, 0.05)
+
+</style>
diff --git a/src/web/app/desktop/views/components/activity.chart.vue b/src/web/app/desktop/views/components/activity.chart.vue
new file mode 100644
index 000000000..e64b181ba
--- /dev/null
+++ b/src/web/app/desktop/views/components/activity.chart.vue
@@ -0,0 +1,101 @@
+<template>
+<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none" @mousedown.prevent="onMousedown">
+	<title>Black ... Total<br/>Blue ... Posts<br/>Red ... Replies<br/>Green ... Reposts</title>
+	<polyline
+		:points="pointsPost"
+		fill="none"
+		stroke-width="1"
+		stroke="#41ddde"/>
+	<polyline
+		:points="pointsReply"
+		fill="none"
+		stroke-width="1"
+		stroke="#f7796c"/>
+	<polyline
+		:points="pointsRepost"
+		fill="none"
+		stroke-width="1"
+		stroke="#a1de41"/>
+	<polyline
+		:points="pointsTotal"
+		fill="none"
+		stroke-width="1"
+		stroke="#555"
+		stroke-dasharray="2 2"/>
+</svg>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+function dragListen(fn) {
+	window.addEventListener('mousemove',  fn);
+	window.addEventListener('mouseleave', dragClear.bind(null, fn));
+	window.addEventListener('mouseup',    dragClear.bind(null, fn));
+}
+
+function dragClear(fn) {
+	window.removeEventListener('mousemove',  fn);
+	window.removeEventListener('mouseleave', dragClear);
+	window.removeEventListener('mouseup',    dragClear);
+}
+
+export default Vue.extend({
+	props: ['data'],
+	data() {
+		return {
+			viewBoxX: 140,
+			viewBoxY: 60,
+			zoom: 1,
+			pos: 0,
+			pointsPost: null,
+			pointsReply: null,
+			pointsRepost: null,
+			pointsTotal: null
+		};
+	},
+	created() {
+		this.data.reverse();
+		this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
+		this.render();
+	},
+	methods: {
+		render() {
+			const peak = Math.max.apply(null, this.data.map(d => d.total));
+			this.pointsPost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' ');
+			this.pointsReply = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
+			this.pointsRepost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' ');
+			this.pointsTotal = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
+		},
+		onMousedown(e) {
+			const clickX = e.clientX;
+			const clickY = e.clientY;
+			const baseZoom = this.zoom;
+			const basePos = this.pos;
+
+			// 動かした時
+			dragListen(me => {
+				let moveLeft = me.clientX - clickX;
+				let moveTop = me.clientY - clickY;
+
+				this.zoom = baseZoom + (-moveTop / 20);
+				this.pos = basePos + moveLeft;
+				if (this.zoom < 1) this.zoom = 1;
+				if (this.pos > 0) this.pos = 0;
+				if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX);
+
+				this.render();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+svg
+	display block
+	padding 10px
+	width 100%
+	cursor all-scroll
+
+</style>
diff --git a/src/web/app/desktop/views/components/activity.vue b/src/web/app/desktop/views/components/activity.vue
new file mode 100644
index 000000000..d1c44f0f5
--- /dev/null
+++ b/src/web/app/desktop/views/components/activity.vue
@@ -0,0 +1,116 @@
+<template>
+<div class="mk-activity">
+	<template v-if="design == 0">
+		<p class="title">%fa:chart-bar%%i18n:desktop.tags.mk-activity-widget.title%</p>
+		<button @click="toggle" title="%i18n:desktop.tags.mk-activity-widget.toggle%">%fa:sort%</button>
+	</template>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<template v-else>
+		<mk-activity-widget-calender v-show="view == 0" :data="[].concat(activity)"/>
+		<mk-activity-widget-chart v-show="view == 1" :data="[].concat(activity)"/>
+	</template>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Calendar from './activity.calendar.vue';
+import Chart from './activity.chart.vue';
+
+export default Vue.extend({
+	components: {
+		'mk-activity-widget-calender': Calendar,
+		'mk-activity-widget-chart': Chart
+	},
+	props: {
+		design: {
+			default: 0
+		},
+		initView: {
+			default: 0
+		},
+		user: {
+			type: Object,
+			required: true
+		}
+	},
+	data() {
+		return {
+			fetching: true,
+			activity: null,
+			view: this.initView
+		};
+	},
+	mounted() {
+		(this as any).api('aggregation/users/activity', {
+			user_id: this.user.id,
+			limit: 20 * 7
+		}).then(activity => {
+			this.activity = activity;
+			this.fetching = false;
+		});
+	},
+	methods: {
+		toggle() {
+			if (this.view == 1) {
+				this.view = 0;
+				this.$emit('viewChanged', this.view);
+			} else {
+				this.view++;
+				this.$emit('viewChanged', this.view);
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-activity
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	&[data-melt]
+		background transparent !important
+		border none !important
+
+	> .title
+		z-index 1
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> [data-fa]
+			margin-right 4px
+
+	> button
+		position absolute
+		z-index 2
+		top 0
+		right 0
+		padding 0
+		width 42px
+		font-size 0.9em
+		line-height 42px
+		color #ccc
+
+		&:hover
+			color #aaa
+
+		&:active
+			color #999
+
+	> .fetching
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+</style>
diff --git a/src/web/app/desktop/views/components/calendar.vue b/src/web/app/desktop/views/components/calendar.vue
index e548a82c5..a21d3e614 100644
--- a/src/web/app/desktop/views/components/calendar.vue
+++ b/src/web/app/desktop/views/components/calendar.vue
@@ -47,7 +47,7 @@ export default Vue.extend({
 			default: 0
 		},
 		start: {
-			type: Object,
+			type: Date,
 			required: false
 		}
 	},
@@ -94,7 +94,7 @@ export default Vue.extend({
 		isOutOfRange(day) {
 			const test = (new Date(this.year, this.month - 1, day)).getTime();
 			return test > this.today.getTime() ||
-				(this.start ? test < this.start.getTime() : false);
+				(this.start ? test < (this.start as any).getTime() : false);
 		},
 
 		isDonichi(day) {
diff --git a/src/web/app/desktop/views/components/followers-window.vue b/src/web/app/desktop/views/components/followers-window.vue
index e56545ccc..ed439114c 100644
--- a/src/web/app/desktop/views/components/followers-window.vue
+++ b/src/web/app/desktop/views/components/followers-window.vue
@@ -1,9 +1,9 @@
 <template>
-<mk-window width='400px' height='550px' @closed="$destroy">
+<mk-window width="400px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">
 		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロワー
 	</span>
-	<mk-user-followers :user="user"/>
+	<mk-followers-list :user="user"/>
 </mk-window>
 </template>
 
diff --git a/src/web/app/desktop/views/components/following-window.vue b/src/web/app/desktop/views/components/following-window.vue
index fa2edfa47..4e1fb0306 100644
--- a/src/web/app/desktop/views/components/following-window.vue
+++ b/src/web/app/desktop/views/components/following-window.vue
@@ -1,9 +1,9 @@
 <template>
-<mk-window width='400px' height='550px' @closed="$destroy">
+<mk-window width="400px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">
 		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロー
 	</span>
-	<mk-user-following :user="user"/>
+	<mk-following-list :user="user"/>
 </mk-window>
 </template>
 
diff --git a/src/web/app/desktop/views/components/friends-maker.vue b/src/web/app/desktop/views/components/friends-maker.vue
index b23373421..61015b979 100644
--- a/src/web/app/desktop/views/components/friends-maker.vue
+++ b/src/web/app/desktop/views/components/friends-maker.vue
@@ -43,8 +43,8 @@ export default Vue.extend({
 				limit: this.limit,
 				offset: this.limit * this.page
 			}).then(users => {
-				this.fetching = false;
 				this.users = users;
+				this.fetching = false;
 			});
 		},
 		refresh() {
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 9a2736954..8e48d67b9 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -34,6 +34,7 @@ import driveNavFolder from './drive-nav-folder.vue';
 import postDetail from './post-detail.vue';
 import settings from './settings.vue';
 import calendar from './calendar.vue';
+import activity from './activity.vue';
 import wNav from './widgets/nav.vue';
 import wCalendar from './widgets/calendar.vue';
 import wPhotoStream from './widgets/photo-stream.vue';
@@ -78,6 +79,7 @@ Vue.component('mk-drive-nav-folder', driveNavFolder);
 Vue.component('mk-post-detail', postDetail);
 Vue.component('mk-settings', settings);
 Vue.component('mk-calendar', calendar);
+Vue.component('mk-activity', activity);
 Vue.component('mkw-nav', wNav);
 Vue.component('mkw-calendar', wCalendar);
 Vue.component('mkw-photo-stream', wPhotoStream);
diff --git a/src/web/app/desktop/views/components/mute-setting.vue b/src/web/app/desktop/views/components/mute-setting.vue
index 3fcc34c9e..fe78401af 100644
--- a/src/web/app/desktop/views/components/mute-setting.vue
+++ b/src/web/app/desktop/views/components/mute-setting.vue
@@ -23,8 +23,8 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('mute/list').then(x => {
-			this.fetching = false;
 			this.users = x.users;
+			this.fetching = false;
 		});
 	}
 });
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index c2c2559f6..429b3549b 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -4,7 +4,7 @@
 		class="read-more"
 		v-if="p.reply && p.reply.reply_id && context == null"
 		title="会話をもっと読み込む"
-		@click="loadContext"
+		@click="fetchContext"
 		:disabled="contextFetching"
 	>
 		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index c63801338..3e0677475 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -57,8 +57,8 @@ export default Vue.extend({
 			(this as any).api('posts/timeline', {
 				until_date: this.date ? this.date.getTime() : undefined
 			}).then(posts => {
-				this.fetching = false;
 				this.posts = posts;
+				this.fetching = false;
 				if (cb) cb();
 			});
 		},
diff --git a/src/web/app/desktop/views/components/users-list.vue b/src/web/app/desktop/views/components/users-list.vue
index 12abb372e..b93a81630 100644
--- a/src/web/app/desktop/views/components/users-list.vue
+++ b/src/web/app/desktop/views/components/users-list.vue
@@ -45,9 +45,9 @@ export default Vue.extend({
 		_fetch(cb) {
 			this.fetching = true;
 			this.fetch(this.mode == 'iknow', this.limit, null, obj => {
-				this.fetching = false;
 				this.users = obj.users;
 				this.next = obj.next;
+				this.fetching = false;
 				if (cb) cb();
 			});
 		},
diff --git a/src/web/app/desktop/views/components/widgets/broadcast.vue b/src/web/app/desktop/views/components/widgets/broadcast.vue
index cdc65a2a7..1a0fd9280 100644
--- a/src/web/app/desktop/views/components/widgets/broadcast.vue
+++ b/src/web/app/desktop/views/components/widgets/broadcast.vue
@@ -46,8 +46,8 @@ export default define({
 					}
 				});
 			}
-			this.fetching = false;
 			this.broadcasts = broadcasts;
+			this.fetching = false;
 		});
 	},
 	methods: {
diff --git a/src/web/app/desktop/views/components/widgets/photo-stream.vue b/src/web/app/desktop/views/components/widgets/photo-stream.vue
index a3f37e8c7..6ad7d2f06 100644
--- a/src/web/app/desktop/views/components/widgets/photo-stream.vue
+++ b/src/web/app/desktop/views/components/widgets/photo-stream.vue
@@ -35,8 +35,8 @@ export default define({
 			type: 'image/*',
 			limit: 9
 		}).then(images => {
-			this.fetching = false;
 			this.images = images;
+			this.fetching = false;
 		});
 	},
 	beforeDestroy() {
diff --git a/src/web/app/desktop/views/components/widgets/slideshow.vue b/src/web/app/desktop/views/components/widgets/slideshow.vue
index beda35066..3c2ef6da4 100644
--- a/src/web/app/desktop/views/components/widgets/slideshow.vue
+++ b/src/web/app/desktop/views/components/widgets/slideshow.vue
@@ -93,8 +93,8 @@ export default define({
 				type: 'image/*',
 				limit: 100
 			}).then(images => {
-				this.fetching = false;
 				this.images = images;
+				this.fetching = false;
 				(this.$refs.slideA as any).style.backgroundImage = '';
 				(this.$refs.slideB as any).style.backgroundImage = '';
 				this.change();
diff --git a/src/web/app/desktop/views/pages/messaging-room.vue b/src/web/app/desktop/views/pages/messaging-room.vue
index 3e4fb256a..ace9e1607 100644
--- a/src/web/app/desktop/views/pages/messaging-room.vue
+++ b/src/web/app/desktop/views/pages/messaging-room.vue
@@ -24,8 +24,8 @@ export default Vue.extend({
 		(this as any).api('users/show', {
 			username: this.username
 		}).then(user => {
-			this.fetching = false;
 			this.user = user;
+			this.fetching = false;
 
 			document.title = 'メッセージ: ' + this.user.name;
 
diff --git a/src/web/app/desktop/views/pages/post.vue b/src/web/app/desktop/views/pages/post.vue
index 186ee332f..8b9f30f10 100644
--- a/src/web/app/desktop/views/pages/post.vue
+++ b/src/web/app/desktop/views/pages/post.vue
@@ -26,8 +26,8 @@ export default Vue.extend({
 		(this as any).api('posts/show', {
 			post_id: this.postId
 		}).then(post => {
-			this.fetching = false;
 			this.post = post;
+			this.fetching = false;
 
 			Progress.done();
 		});
diff --git a/src/web/app/desktop/views/pages/search.vue b/src/web/app/desktop/views/pages/search.vue
index 828aac8fe..b8e8db2e7 100644
--- a/src/web/app/desktop/views/pages/search.vue
+++ b/src/web/app/desktop/views/pages/search.vue
@@ -45,8 +45,8 @@ export default Vue.extend({
 		window.addEventListener('scroll', this.onScroll);
 
 		(this as any).api('posts/search', parse(this.query)).then(posts => {
-			this.fetching = false;
 			this.posts = posts;
+			this.fetching = false;
 		});
 	},
 	beforeDestroy() {
diff --git a/src/web/app/desktop/views/pages/user/user-followers-you-know.vue b/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
index 246ff865d..c58eb75bc 100644
--- a/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
+++ b/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
@@ -27,8 +27,8 @@ export default Vue.extend({
 			iknow: true,
 			limit: 16
 		}).then(x => {
-			this.fetching = false;
 			this.users = x.users;
+			this.fetching = false;
 		});
 	}
 });
diff --git a/src/web/app/desktop/views/pages/user/user-friends.vue b/src/web/app/desktop/views/pages/user/user-friends.vue
index 9f324cfc0..a144ca2ad 100644
--- a/src/web/app/desktop/views/pages/user/user-friends.vue
+++ b/src/web/app/desktop/views/pages/user/user-friends.vue
@@ -2,16 +2,18 @@
 <div class="mk-user-friends">
 	<p class="title">%fa:users%%i18n:desktop.tags.mk-user.frequently-replied-users.title%</p>
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p>
-	<div class="user" v-if="!fetching && users.length != 0" each={ _user in users }>
-		<a class="avatar-anchor" href={ '/' + _user.username }>
-			<img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" v-user-preview={ _user.id }/>
-		</a>
-		<div class="body">
-			<a class="name" href={ '/' + _user.username } v-user-preview={ _user.id }>{ _user.name }</a>
-			<p class="username">@{ _user.username }</p>
+	<template v-if="!fetching && users.length != 0">
+		<div class="user" v-for="friend in users">
+			<router-link class="avatar-anchor" to="`/${friend.username}`">
+				<img class="avatar" :src="`${friend.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/>
+			</router-link>
+			<div class="body">
+				<router-link class="name" to="`/${friend.username}`" v-user-preview="friend.id">{{ friend.name }}</router-link>
+				<p class="username">@{{ friend.username }}</p>
+			</div>
+			<mk-follow-button :user="friend"/>
 		</div>
-		<mk-follow-button user={ _user }/>
-	</div>
+	</template>
 	<p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.frequently-replied-users.no-users%</p>
 </div>
 </template>
@@ -31,8 +33,8 @@ export default Vue.extend({
 			user_id: this.user.id,
 			limit: 4
 		}).then(docs => {
-			this.fetching = false;
 			this.users = docs.map(doc => doc.user);
+			this.fetching = false;
 		});
 	}
 });
diff --git a/src/web/app/desktop/views/pages/user/user-home.vue b/src/web/app/desktop/views/pages/user/user-home.vue
index ca2c68840..5ed901579 100644
--- a/src/web/app/desktop/views/pages/user/user-home.vue
+++ b/src/web/app/desktop/views/pages/user/user-home.vue
@@ -14,8 +14,8 @@
 	</main>
 	<div>
 		<div ref="right">
-			<mk-calendar-widget @warp="warp" :start="new Date(user.created_at)"/>
-			<mk-activity-widget :user="user"/>
+			<mk-calendar @chosen="warp" :start="new Date(user.created_at)"/>
+			<mk-activity :user="user"/>
 			<mk-user-friends :user="user"/>
 			<div class="nav"><mk-nav/></div>
 		</div>
@@ -25,7 +25,20 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import MkUserTimeline from './user-timeline.vue';
+import MkUserProfile from './user-profile.vue';
+import MkUserPhotos from './user-photos.vue';
+import MkUserFollowersYouKnow from './user-followers-you-know.vue';
+import MkUserFriends from './user-friends.vue';
+
 export default Vue.extend({
+	components: {
+		'mk-user-timeline': MkUserTimeline,
+		'mk-user-profile': MkUserProfile,
+		'mk-user-photos': MkUserPhotos,
+		'mk-user-followers-you-know': MkUserFollowersYouKnow,
+		'mk-user-friends': MkUserFriends
+	},
 	props: ['user'],
 	methods: {
 		warp(date) {
diff --git a/src/web/app/desktop/views/pages/user/user-photos.vue b/src/web/app/desktop/views/pages/user/user-photos.vue
index 789d9af85..4029a95cc 100644
--- a/src/web/app/desktop/views/pages/user/user-photos.vue
+++ b/src/web/app/desktop/views/pages/user/user-photos.vue
@@ -3,8 +3,7 @@
 	<p class="title">%fa:camera%%i18n:desktop.tags.mk-user.photos.title%</p>
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.photos.loading%<mk-ellipsis/></p>
 	<div class="stream" v-if="!fetching && images.length > 0">
-		<div v-for="image in images" :key="image.id"
-			class="img"
+		<div v-for="image in images" class="img"
 			:style="`background-image: url(${image.url}?thumbnail&size=256)`"
 		></div>
 	</div>
@@ -28,12 +27,12 @@ export default Vue.extend({
 			with_media: true,
 			limit: 9
 		}).then(posts => {
-			this.fetching = false;
 			posts.forEach(post => {
 				post.media.forEach(media => {
 					if (this.images.length < 9) this.images.push(media);
 				});
 			});
+			this.fetching = false;
 		});
 	}
 });
diff --git a/src/web/app/desktop/views/pages/user/user-profile.vue b/src/web/app/desktop/views/pages/user/user-profile.vue
index d389e01c1..32c28595e 100644
--- a/src/web/app/desktop/views/pages/user/user-profile.vue
+++ b/src/web/app/desktop/views/pages/user/user-profile.vue
@@ -14,7 +14,7 @@
 		<p>%fa:B twitter%<a :href="`https://twitter.com/${user.twitter.screen_name}`" target="_blank">@{{ user.twitter.screen_name }}</a></p>
 	</div>
 	<div class="status">
-	  <p class="posts-count">%fa:angle-right%<a>{{ user.posts_count }}</a><b>投稿</b></p>
+		<p class="posts-count">%fa:angle-right%<a>{{ user.posts_count }}</a><b>投稿</b></p>
 		<p class="following">%fa:angle-right%<a @click="showFollowing">{{ user.following_count }}</a>人を<b>フォロー</b></p>
 		<p class="followers">%fa:angle-right%<a @click="showFollowers">{{ user.followers_count }}</a>人の<b>フォロワー</b></p>
 	</div>
@@ -23,7 +23,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-const age = require('s-age');
+import age from 's-age';
+import MkFollowingWindow from '../../components/following-window.vue';
+import MkFollowersWindow from '../../components/followers-window.vue';
 
 export default Vue.extend({
 	props: ['user'],
@@ -34,8 +36,7 @@ export default Vue.extend({
 	},
 	methods: {
 		showFollowing() {
-			document.body.appendChild(new MkUserFollowingWindow({
-
+			document.body.appendChild(new MkFollowingWindow({
 				propsData: {
 					user: this.user
 				}
@@ -43,8 +44,7 @@ export default Vue.extend({
 		},
 
 		showFollowers() {
-			document.body.appendChild(new MkUserFollowersWindow({
-
+			document.body.appendChild(new MkFollowersWindow({
 				propsData: {
 					user: this.user
 				}
@@ -56,7 +56,7 @@ export default Vue.extend({
 				user_id: this.user.id
 			}).then(() => {
 				this.user.is_muted = true;
-			}, e => {
+			}, () => {
 				alert('error');
 			});
 		},
@@ -66,7 +66,7 @@ export default Vue.extend({
 				user_id: this.user.id
 			}).then(() => {
 				this.user.is_muted = false;
-			}, e => {
+			}, () => {
 				alert('error');
 			});
 		}
diff --git a/src/web/app/desktop/views/components/user-timeline.vue b/src/web/app/desktop/views/pages/user/user-timeline.vue
similarity index 100%
rename from src/web/app/desktop/views/components/user-timeline.vue
rename to src/web/app/desktop/views/pages/user/user-timeline.vue
index fa5b32f22..9dd07653c 100644
--- a/src/web/app/desktop/views/components/user-timeline.vue
+++ b/src/web/app/desktop/views/pages/user/user-timeline.vue
@@ -65,8 +65,8 @@ export default Vue.extend({
 				until_date: this.date ? this.date.getTime() : undefined,
 				with_replies: this.mode == 'with-replies'
 			}).then(posts => {
-				this.fetching = false;
 				this.posts = posts;
+				this.fetching = false;
 				if (cb) cb();
 			});
 		},
diff --git a/src/web/app/desktop/views/pages/user/user.vue b/src/web/app/desktop/views/pages/user/user.vue
index 765057e65..def9ced36 100644
--- a/src/web/app/desktop/views/pages/user/user.vue
+++ b/src/web/app/desktop/views/pages/user/user.vue
@@ -35,8 +35,8 @@ export default Vue.extend({
 		(this as any).api('users/show', {
 			username: this.$route.params.user
 		}).then(user => {
-			this.fetching = false;
 			this.user = user;
+			this.fetching = false;
 			Progress.done();
 			document.title = user.name + ' | Misskey';
 		});
diff --git a/src/web/app/mobile/views/components/drive.vue b/src/web/app/mobile/views/components/drive.vue
index e581d3f05..0e5456332 100644
--- a/src/web/app/mobile/views/components/drive.vue
+++ b/src/web/app/mobile/views/components/drive.vue
@@ -351,13 +351,14 @@ export default Vue.extend({
 			(this as any).api('drive/files/show', {
 				file_id: file
 			}).then(file => {
-				this.fetching = false;
 				this.file = file;
 				this.folder = null;
 				this.hierarchyFolders = [];
 
 				if (file.folder) this.dive(file.folder);
 
+				this.fetching = false;
+
 				this.$emit('open-file', this.file, silent);
 			});
 		},
diff --git a/src/web/app/mobile/views/components/friends-maker.vue b/src/web/app/mobile/views/components/friends-maker.vue
index b069b988c..8e7bf2d63 100644
--- a/src/web/app/mobile/views/components/friends-maker.vue
+++ b/src/web/app/mobile/views/components/friends-maker.vue
@@ -36,8 +36,8 @@ export default Vue.extend({
 				limit: this.limit,
 				offset: this.limit * this.page
 			}).then(users => {
-				this.fetching = false;
 				this.users = users;
+				this.fetching = false;
 			});
 		},
 		refresh() {
diff --git a/src/web/app/mobile/views/components/timeline.vue b/src/web/app/mobile/views/components/timeline.vue
index a04780e94..80fda7560 100644
--- a/src/web/app/mobile/views/components/timeline.vue
+++ b/src/web/app/mobile/views/components/timeline.vue
@@ -63,8 +63,8 @@ export default Vue.extend({
 			(this as any).api('posts/timeline', {
 				until_date: this.date ? (this.date as any).getTime() : undefined
 			}).then(posts => {
-				this.fetching = false;
 				this.posts = posts;
+				this.fetching = false;
 				if (cb) cb();
 			});
 		},
diff --git a/src/web/app/mobile/views/components/user-timeline.vue b/src/web/app/mobile/views/components/user-timeline.vue
index fb2a21419..ffd628838 100644
--- a/src/web/app/mobile/views/components/user-timeline.vue
+++ b/src/web/app/mobile/views/components/user-timeline.vue
@@ -31,8 +31,8 @@ export default Vue.extend({
 			user_id: this.user.id,
 			with_media: this.withMedia
 		}).then(posts => {
-			this.fetching = false;
 			this.posts = posts;
+			this.fetching = false;
 			this.$emit('loaded');
 		});
 	}
diff --git a/src/web/app/mobile/views/components/users-list.vue b/src/web/app/mobile/views/components/users-list.vue
index 45629c558..24c96aec7 100644
--- a/src/web/app/mobile/views/components/users-list.vue
+++ b/src/web/app/mobile/views/components/users-list.vue
@@ -41,9 +41,9 @@ export default Vue.extend({
 		_fetch(cb) {
 			this.fetching = true;
 			this.fetch(this.mode == 'iknow', this.limit, null, obj => {
-				this.fetching = false;
 				this.users = obj.users;
 				this.next = obj.next;
+				this.fetching = false;
 				if (cb) cb();
 			});
 		},
diff --git a/src/web/app/mobile/views/pages/followers.vue b/src/web/app/mobile/views/pages/followers.vue
index e9696dbd3..2f102bd68 100644
--- a/src/web/app/mobile/views/pages/followers.vue
+++ b/src/web/app/mobile/views/pages/followers.vue
@@ -26,8 +26,8 @@ export default Vue.extend({
 		(this as any).api('users/show', {
 			username: this.username
 		}).then(user => {
-			this.fetching = false;
 			this.user = user;
+			this.fetching = false;
 
 			document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
 			document.documentElement.style.background = '#313a42';
diff --git a/src/web/app/mobile/views/pages/following.vue b/src/web/app/mobile/views/pages/following.vue
index c278abfd2..20f085a9f 100644
--- a/src/web/app/mobile/views/pages/following.vue
+++ b/src/web/app/mobile/views/pages/following.vue
@@ -26,8 +26,8 @@ export default Vue.extend({
 		(this as any).api('users/show', {
 			username: this.username
 		}).then(user => {
-			this.fetching = false;
 			this.user = user;
+			this.fetching = false;
 
 			document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
 			document.documentElement.style.background = '#313a42';
diff --git a/src/web/app/mobile/views/pages/post.vue b/src/web/app/mobile/views/pages/post.vue
index c5b6750af..03e9972a4 100644
--- a/src/web/app/mobile/views/pages/post.vue
+++ b/src/web/app/mobile/views/pages/post.vue
@@ -32,8 +32,8 @@ export default Vue.extend({
 		(this as any).api('posts/show', {
 			post_id: this.postId
 		}).then(post => {
-			this.fetching = false;
 			this.post = post;
+			this.fetching = false;
 
 			Progress.done();
 		});
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index f5babbd67..53cde1fb6 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -88,8 +88,8 @@ export default Vue.extend({
 		(this as any).api('users/show', {
 			username: this.username
 		}).then(user => {
-			this.fetching = false;
 			this.user = user;
+			this.fetching = false;
 
 			Progress.done();
 			document.title = user.name + ' | Misskey';
diff --git a/src/web/app/mobile/views/pages/user/home-friends.vue b/src/web/app/mobile/views/pages/user/home-friends.vue
index 4f2f12a64..543ed9b30 100644
--- a/src/web/app/mobile/views/pages/user/home-friends.vue
+++ b/src/web/app/mobile/views/pages/user/home-friends.vue
@@ -22,8 +22,8 @@ export default Vue.extend({
 		(this as any).api('users/get_frequently_replied_users', {
 			user_id: this.user.id
 		}).then(res => {
-			this.fetching = false;
 			this.users = res.map(x => x.user);
+			this.fetching = false;
 		});
 	}
 });
diff --git a/src/web/app/mobile/views/pages/user/home-photos.vue b/src/web/app/mobile/views/pages/user/home-photos.vue
index eb53eb89a..dbb2a410a 100644
--- a/src/web/app/mobile/views/pages/user/home-photos.vue
+++ b/src/web/app/mobile/views/pages/user/home-photos.vue
@@ -28,7 +28,6 @@ export default Vue.extend({
 			with_media: true,
 			limit: 6
 		}).then(posts => {
-			this.fetching = false;
 			posts.forEach(post => {
 				post.media.forEach(media => {
 					if (this.images.length < 9) this.images.push({
@@ -37,6 +36,7 @@ export default Vue.extend({
 					});
 				});
 			});
+			this.fetching = false;
 		});
 	}
 });
diff --git a/src/web/app/mobile/views/pages/user/home-posts.vue b/src/web/app/mobile/views/pages/user/home-posts.vue
index c60f114b8..8b1ea2de5 100644
--- a/src/web/app/mobile/views/pages/user/home-posts.vue
+++ b/src/web/app/mobile/views/pages/user/home-posts.vue
@@ -22,8 +22,8 @@ export default Vue.extend({
 		(this as any).api('users/posts', {
 			user_id: this.user.id
 		}).then(posts => {
-			this.fetching = false;
 			this.posts = posts;
+			this.fetching = false;
 		});
 	}
 });

From 5c03add31b9bbe35626ccd41fd8958460aa1e3a4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 06:13:27 +0900
Subject: [PATCH 200/286] wip

---
 .../desktop/-tags/home-widgets/activity.tag   |  32 -----
 .../desktop/-tags/home-widgets/post-form.tag  | 103 -----------------
 .../views/components/widgets/activity.vue     |  31 +++++
 .../views/components/widgets/post-form.vue    | 109 ++++++++++++++++++
 4 files changed, 140 insertions(+), 135 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/activity.tag
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/post-form.tag
 create mode 100644 src/web/app/desktop/views/components/widgets/activity.vue
 create mode 100644 src/web/app/desktop/views/components/widgets/post-form.vue

diff --git a/src/web/app/desktop/-tags/home-widgets/activity.tag b/src/web/app/desktop/-tags/home-widgets/activity.tag
deleted file mode 100644
index 878de6d13..000000000
--- a/src/web/app/desktop/-tags/home-widgets/activity.tag
+++ /dev/null
@@ -1,32 +0,0 @@
-<mk-activity-home-widget>
-	<mk-activity-widget design={ data.design } view={ data.view } user={ I } ref="activity"/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		this.data = {
-			view: 0,
-			design: 0
-		};
-
-		this.mixin('widget');
-
-		this.initializing = true;
-
-		this.on('mount', () => {
-			this.$refs.activity.on('view-changed', view => {
-				this.data.view = view;
-				this.save();
-			});
-		});
-
-		this.func = () => {
-			if (++this.data.design == 3) this.data.design = 0;
-			this.$refs.activity.update({
-				design: this.data.design
-			});
-			this.save();
-		};
-	</script>
-</mk-activity-home-widget>
diff --git a/src/web/app/desktop/-tags/home-widgets/post-form.tag b/src/web/app/desktop/-tags/home-widgets/post-form.tag
deleted file mode 100644
index 8564cdf02..000000000
--- a/src/web/app/desktop/-tags/home-widgets/post-form.tag
+++ /dev/null
@@ -1,103 +0,0 @@
-<mk-post-form-home-widget>
-	<mk-post-form v-if="place == 'main'"/>
-	<template v-if="place != 'main'">
-		<template v-if="data.design == 0">
-			<p class="title">%fa:pencil-alt%%i18n:desktop.tags.mk-post-form-home-widget.title%</p>
-		</template>
-		<textarea disabled={ posting } ref="text" onkeydown={ onkeydown } placeholder="%i18n:desktop.tags.mk-post-form-home-widget.placeholder%"></textarea>
-		<button @click="post" disabled={ posting }>%i18n:desktop.tags.mk-post-form-home-widget.post%</button>
-	</template>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			overflow hidden
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> .title
-				z-index 1
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> [data-fa]
-					margin-right 4px
-
-			> textarea
-				display block
-				width 100%
-				max-width 100%
-				min-width 100%
-				padding 16px
-				margin-bottom 28px + 16px
-				border none
-				border-bottom solid 1px #eee
-
-			> button
-				display block
-				position absolute
-				bottom 8px
-				right 8px
-				margin 0
-				padding 0 10px
-				height 28px
-				color $theme-color-foreground
-				background $theme-color !important
-				outline none
-				border none
-				border-radius 4px
-				transition background 0.1s ease
-				cursor pointer
-
-				&:hover
-					background lighten($theme-color, 10%) !important
-
-				&:active
-					background darken($theme-color, 10%) !important
-					transition background 0s ease
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			design: 0
-		};
-
-		this.mixin('widget');
-
-		this.func = () => {
-			if (++this.data.design == 2) this.data.design = 0;
-			this.save();
-		};
-
-		this.onkeydown = e => {
-			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
-		};
-
-		this.post = () => {
-			this.update({
-				posting: true
-			});
-
-			this.$root.$data.os.api('posts/create', {
-				text: this.$refs.text.value
-			}).then(data => {
-				this.clear();
-			}).catch(err => {
-				alert('失敗した');
-			}).then(() => {
-				this.update({
-					posting: false
-				});
-			});
-		};
-
-		this.clear = () => {
-			this.$refs.text.value = '';
-		};
-	</script>
-</mk-post-form-home-widget>
diff --git a/src/web/app/desktop/views/components/widgets/activity.vue b/src/web/app/desktop/views/components/widgets/activity.vue
new file mode 100644
index 000000000..8bf45a556
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/activity.vue
@@ -0,0 +1,31 @@
+<template>
+<mk-activity
+	:design="props.design"
+	:init-view="props.view"
+	:user="os.i"
+	@view-changed="viewChanged"/>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+export default define({
+	name: 'activity',
+	props: {
+		design: 0,
+		view: 0
+	}
+}).extend({
+	methods: {
+		func() {
+			if (this.props.design == 2) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		},
+		viewChanged(view) {
+			this.props.view = view;
+		}
+	}
+});
+</script>
diff --git a/src/web/app/desktop/views/components/widgets/post-form.vue b/src/web/app/desktop/views/components/widgets/post-form.vue
new file mode 100644
index 000000000..c32ad5761
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/post-form.vue
@@ -0,0 +1,109 @@
+<template>
+<div class="mkw-post-form">
+	<template v-if="data.design == 0">
+		<p class="title">%fa:pencil-alt%%i18n:desktop.tags.mk-post-form-home-widget.title%</p>
+	</template>
+	<textarea :disabled="posting" v-model="text" @keydown="onKeydown" placeholder="%i18n:desktop.tags.mk-post-form-home-widget.placeholder%"></textarea>
+	<button @click="post" :disabled="posting">%i18n:desktop.tags.mk-post-form-home-widget.post%</button>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+export default define({
+	name: 'post-form',
+	props: {
+		design: 0
+	}
+}).extend({
+	data() {
+		return {
+			posting: false,
+			text: ''
+		};
+	},
+	methods: {
+		func() {
+			if (this.props.design == 1) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		},
+		onKeydown(e) {
+			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
+		},
+		post() {
+			this.posting = true;
+
+			(this as any).api('posts/create', {
+				text: this.text
+			}).then(data => {
+				this.clear();
+			}).catch(err => {
+				alert('失敗した');
+			}).then(() => {
+				this.posting = false;
+			});
+		},
+		clear() {
+			this.text = '';
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-post-form
+	background #fff
+	overflow hidden
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	> .title
+		z-index 1
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> [data-fa]
+			margin-right 4px
+
+	> textarea
+		display block
+		width 100%
+		max-width 100%
+		min-width 100%
+		padding 16px
+		margin-bottom 28px + 16px
+		border none
+		border-bottom solid 1px #eee
+
+	> button
+		display block
+		position absolute
+		bottom 8px
+		right 8px
+		margin 0
+		padding 0 10px
+		height 28px
+		color $theme-color-foreground
+		background $theme-color !important
+		outline none
+		border none
+		border-radius 4px
+		transition background 0.1s ease
+		cursor pointer
+
+		&:hover
+			background lighten($theme-color, 10%) !important
+
+		&:active
+			background darken($theme-color, 10%) !important
+			transition background 0s ease
+
+</style>

From a02bba183306ce3f1e219d88f206b2822f4a5105 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 06:16:45 +0900
Subject: [PATCH 201/286] wip

---
 .../desktop/-tags/home-widgets/version.tag    | 20 -------------
 .../views/components/widgets/version.vue      | 28 +++++++++++++++++++
 2 files changed, 28 insertions(+), 20 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/version.tag
 create mode 100644 src/web/app/desktop/views/components/widgets/version.vue

diff --git a/src/web/app/desktop/-tags/home-widgets/version.tag b/src/web/app/desktop/-tags/home-widgets/version.tag
deleted file mode 100644
index 6dd8ad644..000000000
--- a/src/web/app/desktop/-tags/home-widgets/version.tag
+++ /dev/null
@@ -1,20 +0,0 @@
-<mk-version-home-widget>
-	<p>ver { _VERSION_ } (葵 aoi)</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow visible !important
-
-			> p
-				display block
-				margin 0
-				padding 0 12px
-				text-align center
-				font-size 0.7em
-				color #aaa
-
-	</style>
-	<script lang="typescript">
-		this.mixin('widget');
-	</script>
-</mk-version-home-widget>
diff --git a/src/web/app/desktop/views/components/widgets/version.vue b/src/web/app/desktop/views/components/widgets/version.vue
new file mode 100644
index 000000000..ad2b27bc4
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/version.vue
@@ -0,0 +1,28 @@
+<template>
+<p>ver {{ v }} (葵 aoi)</p>
+</template>
+
+<script lang="ts">
+import { version } from '../../../../config';
+import define from '../../../../common/define-widget';
+export default define({
+	name: 'version'
+}).extend({
+	data() {
+		return {
+			v: version
+		};
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+p
+	display block
+	margin 0
+	padding 0 12px
+	text-align center
+	font-size 0.7em
+	color #aaa
+
+</style>

From 033eca0d78f8a27ad818f0095831051140e6a8d2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 06:27:35 +0900
Subject: [PATCH 202/286] wip

---
 .../-tags/home-widgets/recommended-polls.tag  | 119 -----------------
 .../views/components/widgets/polls.vue        | 122 ++++++++++++++++++
 2 files changed, 122 insertions(+), 119 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/recommended-polls.tag
 create mode 100644 src/web/app/desktop/views/components/widgets/polls.vue

diff --git a/src/web/app/desktop/-tags/home-widgets/recommended-polls.tag b/src/web/app/desktop/-tags/home-widgets/recommended-polls.tag
deleted file mode 100644
index 43c6096a3..000000000
--- a/src/web/app/desktop/-tags/home-widgets/recommended-polls.tag
+++ /dev/null
@@ -1,119 +0,0 @@
-<mk-recommended-polls-home-widget>
-	<template v-if="!data.compact">
-		<p class="title">%fa:chart-pie%%i18n:desktop.tags.mk-recommended-polls-home-widget.title%</p>
-		<button @click="fetch" title="%i18n:desktop.tags.mk-recommended-polls-home-widget.refresh%">%fa:sync%</button>
-	</template>
-	<div class="poll" v-if="!loading && poll != null">
-		<p v-if="poll.text"><a href="/{ poll.user.username }/{ poll.id }">{ poll.text }</a></p>
-		<p v-if="!poll.text"><a href="/{ poll.user.username }/{ poll.id }">%fa:link%</a></p>
-		<mk-poll post={ poll }/>
-	</div>
-	<p class="empty" v-if="!loading && poll == null">%i18n:desktop.tags.mk-recommended-polls-home-widget.nothing%</p>
-	<p class="loading" v-if="loading">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> .title
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				border-bottom solid 1px #eee
-
-				> [data-fa]
-					margin-right 4px
-
-			> button
-				position absolute
-				z-index 2
-				top 0
-				right 0
-				padding 0
-				width 42px
-				font-size 0.9em
-				line-height 42px
-				color #ccc
-
-				&:hover
-					color #aaa
-
-				&:active
-					color #999
-
-			> .poll
-				padding 16px
-				font-size 12px
-				color #555
-
-				> p
-					margin 0 0 8px 0
-
-					> a
-						color inherit
-
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-			> .loading
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			compact: false
-		};
-
-		this.mixin('widget');
-
-		this.poll = null;
-		this.loading = true;
-
-		this.offset = 0;
-
-		this.on('mount', () => {
-			this.fetch();
-		});
-
-		this.fetch = () => {
-			this.update({
-				loading: true,
-				poll: null
-			});
-			this.$root.$data.os.api('posts/polls/recommendation', {
-				limit: 1,
-				offset: this.offset
-			}).then(posts => {
-				const poll = posts ? posts[0] : null;
-				if (poll == null) {
-					this.offset = 0;
-				} else {
-					this.offset++;
-				}
-				this.update({
-					loading: false,
-					poll: poll
-				});
-			});
-		};
-
-		this.func = () => {
-			this.data.compact = !this.data.compact;
-			this.save();
-		};
-	</script>
-</mk-recommended-polls-home-widget>
diff --git a/src/web/app/desktop/views/components/widgets/polls.vue b/src/web/app/desktop/views/components/widgets/polls.vue
new file mode 100644
index 000000000..71d5391b1
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/polls.vue
@@ -0,0 +1,122 @@
+<template>
+<div class="mkw-polls">
+	<template v-if="!props.compact">
+		<p class="title">%fa:chart-pie%%i18n:desktop.tags.mk-recommended-polls-home-widget.title%</p>
+		<button @click="fetch" title="%i18n:desktop.tags.mk-recommended-polls-home-widget.refresh%">%fa:sync%</button>
+	</template>
+	<div class="poll" v-if="!fetching && poll != null">
+		<p v-if="poll.text"><router-link to="`/${ poll.user.username }/${ poll.id }`">{{ poll.text }}</router-link></p>
+		<p v-if="!poll.text"><router-link to="`/${ poll.user.username }/${ poll.id }`">%fa:link%</router-link></p>
+		<mk-poll :post="poll"/>
+	</div>
+	<p class="empty" v-if="!fetching && poll == null">%i18n:desktop.tags.mk-recommended-polls-home-widget.nothing%</p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+export default define({
+	name: 'polls',
+	props: {
+		compact: false
+	}
+}).extend({
+	data() {
+		return {
+			poll: null,
+			fetching: true,
+			offset: 0
+		};
+	},
+	mounted() {
+		this.fetch();
+	},
+	methods: {
+		func() {
+			this.props.compact = !this.props.compact;
+		},
+		fetch() {
+			this.fetching = true;
+			this.poll = null;
+
+			(this as any).api('posts/polls/recommendation', {
+				limit: 1,
+				offset: this.offset
+			}).then(posts => {
+				const poll = posts ? posts[0] : null;
+				if (poll == null) {
+					this.offset = 0;
+				} else {
+					this.offset++;
+				}
+				this.poll = poll;
+				this.fetching = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-polls
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	> .title
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		border-bottom solid 1px #eee
+
+		> [data-fa]
+			margin-right 4px
+
+	> button
+		position absolute
+		z-index 2
+		top 0
+		right 0
+		padding 0
+		width 42px
+		font-size 0.9em
+		line-height 42px
+		color #ccc
+
+		&:hover
+			color #aaa
+
+		&:active
+			color #999
+
+	> .poll
+		padding 16px
+		font-size 12px
+		color #555
+
+		> p
+			margin 0 0 8px 0
+
+			> a
+				color inherit
+
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+	> .fetching
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+</style>

From bce6616af993ca6c91128c76de47dbbbfac80813 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 06:40:31 +0900
Subject: [PATCH 203/286] wip

---
 .../desktop/-tags/home-widgets/rss-reader.tag | 109 -----------------
 .../desktop/views/components/widgets/rss.vue  | 111 ++++++++++++++++++
 2 files changed, 111 insertions(+), 109 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/rss-reader.tag
 create mode 100644 src/web/app/desktop/views/components/widgets/rss.vue

diff --git a/src/web/app/desktop/-tags/home-widgets/rss-reader.tag b/src/web/app/desktop/-tags/home-widgets/rss-reader.tag
deleted file mode 100644
index 4e0ed702e..000000000
--- a/src/web/app/desktop/-tags/home-widgets/rss-reader.tag
+++ /dev/null
@@ -1,109 +0,0 @@
-<mk-rss-reader-home-widget>
-	<template v-if="!data.compact">
-		<p class="title">%fa:rss-square%RSS</p>
-		<button @click="settings" title="設定">%fa:cog%</button>
-	</template>
-	<div class="feed" v-if="!initializing">
-		<template each={ item in items }><a href={ item.link } target="_blank">{ item.title }</a></template>
-	</div>
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> .title
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> [data-fa]
-					margin-right 4px
-
-			> button
-				position absolute
-				top 0
-				right 0
-				padding 0
-				width 42px
-				font-size 0.9em
-				line-height 42px
-				color #ccc
-
-				&:hover
-					color #aaa
-
-				&:active
-					color #999
-
-			> .feed
-				padding 12px 16px
-				font-size 0.9em
-
-				> a
-					display block
-					padding 4px 0
-					color #666
-					border-bottom dashed 1px #eee
-
-					&:last-child
-						border-bottom none
-
-			> .initializing
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			compact: false
-		};
-
-		this.mixin('widget');
-
-		this.url = 'http://news.yahoo.co.jp/pickup/rss.xml';
-		this.items = [];
-		this.initializing = true;
-
-		this.on('mount', () => {
-			this.fetch();
-			this.clock = setInterval(this.fetch, 60000);
-		});
-
-		this.on('unmount', () => {
-			clearInterval(this.clock);
-		});
-
-		this.fetch = () => {
-			fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.url}`, {
-				cache: 'no-cache'
-			}).then(res => {
-				res.json().then(feed => {
-					this.update({
-						initializing: false,
-						items: feed.items
-					});
-				});
-			});
-		};
-
-		this.settings = () => {
-		};
-
-		this.func = () => {
-			this.data.compact = !this.data.compact;
-			this.save();
-		};
-	</script>
-</mk-rss-reader-home-widget>
diff --git a/src/web/app/desktop/views/components/widgets/rss.vue b/src/web/app/desktop/views/components/widgets/rss.vue
new file mode 100644
index 000000000..954edf3c5
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/rss.vue
@@ -0,0 +1,111 @@
+<template>
+<div class="mkw-rss">
+	<template v-if="!props.compact">
+		<p class="title">%fa:rss-square%RSS</p>
+		<button title="設定">%fa:cog%</button>
+	</template>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<div class="feed" v-else>
+		<a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+export default define({
+	name: 'rss',
+	props: {
+		compact: false
+	}
+}).extend({
+	data() {
+		return {
+			url: 'http://news.yahoo.co.jp/pickup/rss.xml',
+			items: [],
+			fetching: true,
+			clock: null
+		};
+	},
+	mounted() {
+		this.fetch();
+		this.clock = setInterval(this.fetch, 60000);
+	},
+	beforeDestroy() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		func() {
+			this.props.compact = !this.props.compact;
+		},
+		fetch() {
+			fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.url}`, {
+				cache: 'no-cache'
+			}).then(res => {
+				res.json().then(feed => {
+					this.items = feed.items;
+					this.fetching = false;
+				});
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-rss
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	> .title
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> [data-fa]
+			margin-right 4px
+
+	> button
+		position absolute
+		top 0
+		right 0
+		padding 0
+		width 42px
+		font-size 0.9em
+		line-height 42px
+		color #ccc
+
+		&:hover
+			color #aaa
+
+		&:active
+			color #999
+
+	> .feed
+		padding 12px 16px
+		font-size 0.9em
+
+		> a
+			display block
+			padding 4px 0
+			color #666
+			border-bottom dashed 1px #eee
+
+			&:last-child
+				border-bottom none
+
+	> .fetching
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+</style>

From b4a874766e460087666e233a60ea22b149b70097 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 07:05:16 +0900
Subject: [PATCH 204/286] wip

---
 .../home-widgets/user-recommendation.tag      | 165 -----------------
 .../views/components/widgets/users.vue        | 170 ++++++++++++++++++
 2 files changed, 170 insertions(+), 165 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/user-recommendation.tag
 create mode 100644 src/web/app/desktop/views/components/widgets/users.vue

diff --git a/src/web/app/desktop/-tags/home-widgets/user-recommendation.tag b/src/web/app/desktop/-tags/home-widgets/user-recommendation.tag
deleted file mode 100644
index b2a19d71f..000000000
--- a/src/web/app/desktop/-tags/home-widgets/user-recommendation.tag
+++ /dev/null
@@ -1,165 +0,0 @@
-<mk-user-recommendation-home-widget>
-	<template v-if="!data.compact">
-		<p class="title">%fa:users%%i18n:desktop.tags.mk-user-recommendation-home-widget.title%</p>
-		<button @click="refresh" title="%i18n:desktop.tags.mk-user-recommendation-home-widget.refresh%">%fa:sync%</button>
-	</template>
-	<div class="user" v-if="!loading && users.length != 0" each={ _user in users }>
-		<a class="avatar-anchor" href={ '/' + _user.username }>
-			<img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" v-user-preview={ _user.id }/>
-		</a>
-		<div class="body">
-			<a class="name" href={ '/' + _user.username } v-user-preview={ _user.id }>{ _user.name }</a>
-			<p class="username">@{ _user.username }</p>
-		</div>
-		<mk-follow-button user={ _user }/>
-	</div>
-	<p class="empty" v-if="!loading && users.length == 0">%i18n:desktop.tags.mk-user-recommendation-home-widget.no-one%</p>
-	<p class="loading" v-if="loading">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> .title
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				border-bottom solid 1px #eee
-
-				> [data-fa]
-					margin-right 4px
-
-			> button
-				position absolute
-				z-index 2
-				top 0
-				right 0
-				padding 0
-				width 42px
-				font-size 0.9em
-				line-height 42px
-				color #ccc
-
-				&:hover
-					color #aaa
-
-				&:active
-					color #999
-
-			> .user
-				padding 16px
-				border-bottom solid 1px #eee
-
-				&:last-child
-					border-bottom none
-
-				&:after
-					content ""
-					display block
-					clear both
-
-				> .avatar-anchor
-					display block
-					float left
-					margin 0 12px 0 0
-
-					> .avatar
-						display block
-						width 42px
-						height 42px
-						margin 0
-						border-radius 8px
-						vertical-align bottom
-
-				> .body
-					float left
-					width calc(100% - 54px)
-
-					> .name
-						margin 0
-						font-size 16px
-						line-height 24px
-						color #555
-
-					> .username
-						display block
-						margin 0
-						font-size 15px
-						line-height 16px
-						color #ccc
-
-				> mk-follow-button
-					position absolute
-					top 16px
-					right 16px
-
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-			> .loading
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			compact: false
-		};
-
-		this.mixin('widget');
-		this.mixin('user-preview');
-
-		this.users = null;
-		this.loading = true;
-
-		this.limit = 3;
-		this.page = 0;
-
-		this.on('mount', () => {
-			this.fetch();
-		});
-
-		this.fetch = () => {
-			this.update({
-				loading: true,
-				users: null
-			});
-			this.$root.$data.os.api('users/recommendation', {
-				limit: this.limit,
-				offset: this.limit * this.page
-			}).then(users => {
-				this.update({
-					loading: false,
-					users: users
-				});
-			});
-		};
-
-		this.refresh = () => {
-			if (this.users.length < this.limit) {
-				this.page = 0;
-			} else {
-				this.page++;
-			}
-			this.fetch();
-		};
-
-		this.func = () => {
-			this.data.compact = !this.data.compact;
-			this.save();
-		};
-	</script>
-</mk-user-recommendation-home-widget>
diff --git a/src/web/app/desktop/views/components/widgets/users.vue b/src/web/app/desktop/views/components/widgets/users.vue
new file mode 100644
index 000000000..6876d0bf0
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/users.vue
@@ -0,0 +1,170 @@
+<template>
+<div class="mkw-users">
+	<template v-if="!props.compact">
+		<p class="title">%fa:users%%i18n:desktop.tags.mk-user-recommendation-home-widget.title%</p>
+		<button @click="refresh" title="%i18n:desktop.tags.mk-user-recommendation-home-widget.refresh%">%fa:sync%</button>
+	</template>
+	<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" :href="`/${_user.username}`">
+				<img class="avatar" :src="`${_user.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
+			</router-link>
+			<div class="body">
+				<a class="name" :href="`/${_user.username}`" v-user-preview="_user.id">{{ _user.name }}</a>
+				<p class="username">@{{ _user.username }}</p>
+			</div>
+			<mk-follow-button :user="_user"/>
+		</div>
+	</template>
+	<p class="empty" v-else>%i18n:desktop.tags.mk-user-recommendation-home-widget.no-one%</p>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+
+const limit = 3;
+
+export default define({
+	name: 'users',
+	props: {
+		compact: false
+	}
+}).extend({
+	data() {
+		return {
+			users: [],
+			fetching: true,
+			page: 0
+		};
+	},
+	mounted() {
+		this.fetch();
+	},
+	methods: {
+		func() {
+			this.props.compact = !this.props.compact;
+		},
+		fetch() {
+			this.fetching = true;
+			this.users = [];
+
+			(this as any).api('users/recommendation', {
+				limit: limit,
+				offset: limit * this.page
+			}).then(users => {
+				this.users = users;
+				this.fetching = false;
+			});
+		},
+		refresh() {
+			if (this.users.length < limit) {
+				this.page = 0;
+			} else {
+				this.page++;
+			}
+			this.fetch();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-users
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	> .title
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		border-bottom solid 1px #eee
+
+		> [data-fa]
+			margin-right 4px
+
+	> button
+		position absolute
+		z-index 2
+		top 0
+		right 0
+		padding 0
+		width 42px
+		font-size 0.9em
+		line-height 42px
+		color #ccc
+
+		&:hover
+			color #aaa
+
+		&:active
+			color #999
+
+	> .user
+		padding 16px
+		border-bottom solid 1px #eee
+
+		&:last-child
+			border-bottom none
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		> .avatar-anchor
+			display block
+			float left
+			margin 0 12px 0 0
+
+			> .avatar
+				display block
+				width 42px
+				height 42px
+				margin 0
+				border-radius 8px
+				vertical-align bottom
+
+		> .body
+			float left
+			width calc(100% - 54px)
+
+			> .name
+				margin 0
+				font-size 16px
+				line-height 24px
+				color #555
+
+			> .username
+				display block
+				margin 0
+				font-size 15px
+				line-height 16px
+				color #ccc
+
+		> .mk-follow-button
+			position absolute
+			top 16px
+			right 16px
+
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+	> .fetching
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+</style>

From bc17a0b2cb3e789d79e277ecd64a10ae1ac2e7a5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 07:56:39 +0900
Subject: [PATCH 205/286] wip

---
 src/web/app/common/scripts/bytes-to-size.ts   |   6 -
 .../app/desktop/-tags/home-widgets/server.tag | 533 ------------------
 .../components/widgets/server.cpu-memory.vue  | 127 +++++
 .../views/components/widgets/server.cpu.vue   |  68 +++
 .../views/components/widgets/server.disk.vue  |  76 +++
 .../views/components/widgets/server.info.vue  |  25 +
 .../components/widgets/server.memory.vue      |  76 +++
 .../views/components/widgets/server.pie.vue   |  61 ++
 .../components/widgets/server.uptimes.vue     |  46 ++
 .../views/components/widgets/server.vue       | 127 +++++
 src/web/app/filters/bytes.ts                  |   8 +
 src/web/app/filters/index.ts                  |   1 +
 src/web/app/init.ts                           |   3 +
 13 files changed, 618 insertions(+), 539 deletions(-)
 delete mode 100644 src/web/app/common/scripts/bytes-to-size.ts
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/server.tag
 create mode 100644 src/web/app/desktop/views/components/widgets/server.cpu-memory.vue
 create mode 100644 src/web/app/desktop/views/components/widgets/server.cpu.vue
 create mode 100644 src/web/app/desktop/views/components/widgets/server.disk.vue
 create mode 100644 src/web/app/desktop/views/components/widgets/server.info.vue
 create mode 100644 src/web/app/desktop/views/components/widgets/server.memory.vue
 create mode 100644 src/web/app/desktop/views/components/widgets/server.pie.vue
 create mode 100644 src/web/app/desktop/views/components/widgets/server.uptimes.vue
 create mode 100644 src/web/app/desktop/views/components/widgets/server.vue
 create mode 100644 src/web/app/filters/bytes.ts
 create mode 100644 src/web/app/filters/index.ts

diff --git a/src/web/app/common/scripts/bytes-to-size.ts b/src/web/app/common/scripts/bytes-to-size.ts
deleted file mode 100644
index 1d2b1e7ce..000000000
--- a/src/web/app/common/scripts/bytes-to-size.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export default (bytes, digits = 0) => {
-	const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
-	if (bytes == 0) return '0Byte';
-	const i = Math.floor(Math.log(bytes) / Math.log(1024));
-	return (bytes / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
-};
diff --git a/src/web/app/desktop/-tags/home-widgets/server.tag b/src/web/app/desktop/-tags/home-widgets/server.tag
deleted file mode 100644
index 992517163..000000000
--- a/src/web/app/desktop/-tags/home-widgets/server.tag
+++ /dev/null
@@ -1,533 +0,0 @@
-<mk-server-home-widget data-melt={ data.design == 2 }>
-	<template v-if="data.design == 0">
-		<p class="title">%fa:server%%i18n:desktop.tags.mk-server-home-widget.title%</p>
-		<button @click="toggle" title="%i18n:desktop.tags.mk-server-home-widget.toggle%">%fa:sort%</button>
-	</template>
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<mk-server-home-widget-cpu-and-memory-usage v-if="!initializing" show={ data.view == 0 } connection={ connection }/>
-	<mk-server-home-widget-cpu v-if="!initializing" show={ data.view == 1 } connection={ connection } meta={ meta }/>
-	<mk-server-home-widget-memory v-if="!initializing" show={ data.view == 2 } connection={ connection }/>
-	<mk-server-home-widget-disk v-if="!initializing" show={ data.view == 3 } connection={ connection }/>
-	<mk-server-home-widget-uptimes v-if="!initializing" show={ data.view == 4 } connection={ connection }/>
-	<mk-server-home-widget-info v-if="!initializing" show={ data.view == 5 } connection={ connection } meta={ meta }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			&[data-melt]
-				background transparent !important
-				border none !important
-
-			> .title
-				z-index 1
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> [data-fa]
-					margin-right 4px
-
-			> button
-				position absolute
-				z-index 2
-				top 0
-				right 0
-				padding 0
-				width 42px
-				font-size 0.9em
-				line-height 42px
-				color #ccc
-
-				&:hover
-					color #aaa
-
-				&:active
-					color #999
-
-			> .initializing
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('os');
-
-		this.data = {
-			view: 0,
-			design: 0
-		};
-
-		this.mixin('widget');
-
-		this.mixin('server-stream');
-		this.connection = this.serverStream.getConnection();
-		this.connectionId = this.serverStream.use();
-
-		this.initializing = true;
-
-		this.on('mount', () => {
-			this.mios.getMeta().then(meta => {
-				this.update({
-					initializing: false,
-					meta
-				});
-			});
-		});
-
-		this.on('unmount', () => {
-			this.serverStream.dispose(this.connectionId);
-		});
-
-		this.toggle = () => {
-			this.data.view++;
-			if (this.data.view == 6) this.data.view = 0;
-
-			// Save widget state
-			this.save();
-		};
-
-		this.func = () => {
-			if (++this.data.design == 3) this.data.design = 0;
-			this.save();
-		};
-	</script>
-</mk-server-home-widget>
-
-<mk-server-home-widget-cpu-and-memory-usage>
-	<svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none">
-		<defs>
-			<linearGradient id={ cpuGradientId } x1="0" x2="0" y1="1" y2="0">
-				<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
-				<stop offset="33%" stop-color="hsl(120, 80%, 70%)"></stop>
-				<stop offset="66%" stop-color="hsl(60, 80%, 70%)"></stop>
-				<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
-			</linearGradient>
-			<mask id={ cpuMaskId } x="0" y="0" riot-width={ viewBoxX } riot-height={ viewBoxY }>
-				<polygon
-					riot-points={ cpuPolygonPoints }
-					fill="#fff"
-					fill-opacity="0.5"/>
-				<polyline
-					riot-points={ cpuPolylinePoints }
-					fill="none"
-					stroke="#fff"
-					stroke-width="1"/>
-			</mask>
-		</defs>
-		<rect
-			x="-1" y="-1"
-			riot-width={ viewBoxX + 2 } riot-height={ viewBoxY + 2 }
-			style="stroke: none; fill: url(#{ cpuGradientId }); mask: url(#{ cpuMaskId })"/>
-		<text x="1" y="5">CPU <tspan>{ cpuP }%</tspan></text>
-	</svg>
-	<svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none">
-		<defs>
-			<linearGradient id={ memGradientId } x1="0" x2="0" y1="1" y2="0">
-				<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
-				<stop offset="33%" stop-color="hsl(120, 80%, 70%)"></stop>
-				<stop offset="66%" stop-color="hsl(60, 80%, 70%)"></stop>
-				<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
-			</linearGradient>
-			<mask id={ memMaskId } x="0" y="0" riot-width={ viewBoxX } riot-height={ viewBoxY }>
-				<polygon
-					riot-points={ memPolygonPoints }
-					fill="#fff"
-					fill-opacity="0.5"/>
-				<polyline
-					riot-points={ memPolylinePoints }
-					fill="none"
-					stroke="#fff"
-					stroke-width="1"/>
-			</mask>
-		</defs>
-		<rect
-			x="-1" y="-1"
-			riot-width={ viewBoxX + 2 } riot-height={ viewBoxY + 2 }
-			style="stroke: none; fill: url(#{ memGradientId }); mask: url(#{ memMaskId })"/>
-		<text x="1" y="5">MEM <tspan>{ memP }%</tspan></text>
-	</svg>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> svg
-				display block
-				padding 10px
-				width 50%
-				float left
-
-				&:first-child
-					padding-right 5px
-
-				&:last-child
-					padding-left 5px
-
-				> text
-					font-size 5px
-					fill rgba(0, 0, 0, 0.55)
-
-					> tspan
-						opacity 0.5
-
-			&:after
-				content ""
-				display block
-				clear both
-	</style>
-	<script lang="typescript">
-		import uuid from 'uuid';
-
-		this.viewBoxX = 50;
-		this.viewBoxY = 30;
-		this.stats = [];
-		this.connection = this.opts.connection;
-		this.cpuGradientId = uuid();
-		this.cpuMaskId = uuid();
-		this.memGradientId = uuid();
-		this.memMaskId = uuid();
-
-		this.on('mount', () => {
-			this.connection.on('stats', this.onStats);
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('stats', this.onStats);
-		});
-
-		this.onStats = stats => {
-			stats.mem.used = stats.mem.total - stats.mem.free;
-			this.stats.push(stats);
-			if (this.stats.length > 50) this.stats.shift();
-
-			const cpuPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - s.cpu_usage) * this.viewBoxY}`).join(' ');
-			const memPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.mem.used / s.mem.total)) * this.viewBoxY}`).join(' ');
-
-			const cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ cpuPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
-			const memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ memPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
-
-			const cpuP = (stats.cpu_usage * 100).toFixed(0);
-			const memP = (stats.mem.used / stats.mem.total * 100).toFixed(0);
-
-			this.update({
-				cpuPolylinePoints,
-				memPolylinePoints,
-				cpuPolygonPoints,
-				memPolygonPoints,
-				cpuP,
-				memP
-			});
-		};
-	</script>
-</mk-server-home-widget-cpu-and-memory-usage>
-
-<mk-server-home-widget-cpu>
-	<mk-server-home-widget-pie ref="pie"/>
-	<div>
-		<p>%fa:microchip%CPU</p>
-		<p>{ cores } Cores</p>
-		<p>{ model }</p>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> mk-server-home-widget-pie
-				padding 10px
-				height 100px
-				float left
-
-			> div
-				float left
-				width calc(100% - 100px)
-				padding 10px 10px 10px 0
-
-				> p
-					margin 0
-					font-size 12px
-					color #505050
-
-					&:first-child
-						font-weight bold
-
-						> [data-fa]
-							margin-right 4px
-
-			&:after
-				content ""
-				display block
-				clear both
-
-	</style>
-	<script lang="typescript">
-		this.cores = this.opts.meta.cpu.cores;
-		this.model = this.opts.meta.cpu.model;
-		this.connection = this.opts.connection;
-
-		this.on('mount', () => {
-			this.connection.on('stats', this.onStats);
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('stats', this.onStats);
-		});
-
-		this.onStats = stats => {
-			this.$refs.pie.render(stats.cpu_usage);
-		};
-	</script>
-</mk-server-home-widget-cpu>
-
-<mk-server-home-widget-memory>
-	<mk-server-home-widget-pie ref="pie"/>
-	<div>
-		<p>%fa:flask%Memory</p>
-		<p>Total: { bytesToSize(total, 1) }</p>
-		<p>Used: { bytesToSize(used, 1) }</p>
-		<p>Free: { bytesToSize(free, 1) }</p>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> mk-server-home-widget-pie
-				padding 10px
-				height 100px
-				float left
-
-			> div
-				float left
-				width calc(100% - 100px)
-				padding 10px 10px 10px 0
-
-				> p
-					margin 0
-					font-size 12px
-					color #505050
-
-					&:first-child
-						font-weight bold
-
-						> [data-fa]
-							margin-right 4px
-
-			&:after
-				content ""
-				display block
-				clear both
-
-	</style>
-	<script lang="typescript">
-		import bytesToSize from '../../../common/scripts/bytes-to-size';
-
-		this.connection = this.opts.connection;
-		this.bytesToSize = bytesToSize;
-
-		this.on('mount', () => {
-			this.connection.on('stats', this.onStats);
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('stats', this.onStats);
-		});
-
-		this.onStats = stats => {
-			stats.mem.used = stats.mem.total - stats.mem.free;
-			this.$refs.pie.render(stats.mem.used / stats.mem.total);
-
-			this.update({
-				total: stats.mem.total,
-				used: stats.mem.used,
-				free: stats.mem.free
-			});
-		};
-	</script>
-</mk-server-home-widget-memory>
-
-<mk-server-home-widget-disk>
-	<mk-server-home-widget-pie ref="pie"/>
-	<div>
-		<p>%fa:R hdd%Storage</p>
-		<p>Total: { bytesToSize(total, 1) }</p>
-		<p>Available: { bytesToSize(available, 1) }</p>
-		<p>Used: { bytesToSize(used, 1) }</p>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> mk-server-home-widget-pie
-				padding 10px
-				height 100px
-				float left
-
-			> div
-				float left
-				width calc(100% - 100px)
-				padding 10px 10px 10px 0
-
-				> p
-					margin 0
-					font-size 12px
-					color #505050
-
-					&:first-child
-						font-weight bold
-
-						> [data-fa]
-							margin-right 4px
-
-			&:after
-				content ""
-				display block
-				clear both
-
-	</style>
-	<script lang="typescript">
-		import bytesToSize from '../../../common/scripts/bytes-to-size';
-
-		this.connection = this.opts.connection;
-		this.bytesToSize = bytesToSize;
-
-		this.on('mount', () => {
-			this.connection.on('stats', this.onStats);
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('stats', this.onStats);
-		});
-
-		this.onStats = stats => {
-			stats.disk.used = stats.disk.total - stats.disk.free;
-
-			this.$refs.pie.render(stats.disk.used / stats.disk.total);
-
-			this.update({
-				total: stats.disk.total,
-				used: stats.disk.used,
-				available: stats.disk.available
-			});
-		};
-	</script>
-</mk-server-home-widget-disk>
-
-<mk-server-home-widget-uptimes>
-	<p>Uptimes</p>
-	<p>Process: { process ? process.toFixed(0) : '---' }s</p>
-	<p>OS: { os ? os.toFixed(0) : '---' }s</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding 10px 14px
-
-			> p
-				margin 0
-				font-size 12px
-				color #505050
-
-				&:first-child
-					font-weight bold
-
-	</style>
-	<script lang="typescript">
-		this.connection = this.opts.connection;
-
-		this.on('mount', () => {
-			this.connection.on('stats', this.onStats);
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('stats', this.onStats);
-		});
-
-		this.onStats = stats => {
-			this.update({
-				process: stats.process_uptime,
-				os: stats.os_uptime
-			});
-		};
-	</script>
-</mk-server-home-widget-uptimes>
-
-<mk-server-home-widget-info>
-	<p>Maintainer: <b>{ meta.maintainer }</b></p>
-	<p>Machine: { meta.machine }</p>
-	<p>Node: { meta.node }</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding 10px 14px
-
-			> p
-				margin 0
-				font-size 12px
-				color #505050
-
-	</style>
-	<script lang="typescript">
-		this.meta = this.opts.meta;
-	</script>
-</mk-server-home-widget-info>
-
-<mk-server-home-widget-pie>
-	<svg viewBox="0 0 1 1" preserveAspectRatio="none">
-		<circle
-			riot-r={ r }
-			cx="50%" cy="50%"
-			fill="none"
-			stroke-width="0.1"
-			stroke="rgba(0, 0, 0, 0.05)"/>
-		<circle
-			riot-r={ r }
-			cx="50%" cy="50%"
-			riot-stroke-dasharray={ Math.PI * (r * 2) }
-			riot-stroke-dashoffset={ strokeDashoffset }
-			fill="none"
-			stroke-width="0.1"
-			riot-stroke={ color }/>
-		<text x="50%" y="50%" dy="0.05" text-anchor="middle">{ (p * 100).toFixed(0) }%</text>
-	</svg>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> svg
-				display block
-				height 100%
-
-				> circle
-					transform-origin center
-					transform rotate(-90deg)
-					transition stroke-dashoffset 0.5s ease
-
-				> text
-					font-size 0.15px
-					fill rgba(0, 0, 0, 0.6)
-
-	</style>
-	<script lang="typescript">
-		this.r = 0.4;
-
-		this.render = p => {
-			const color = `hsl(${180 - (p * 180)}, 80%, 70%)`;
-			const strokeDashoffset = (1 - p) * (Math.PI * (this.r * 2));
-
-			this.update({
-				p,
-				color,
-				strokeDashoffset
-			});
-		};
-	</script>
-</mk-server-home-widget-pie>
diff --git a/src/web/app/desktop/views/components/widgets/server.cpu-memory.vue b/src/web/app/desktop/views/components/widgets/server.cpu-memory.vue
new file mode 100644
index 000000000..00b3dc3af
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/server.cpu-memory.vue
@@ -0,0 +1,127 @@
+<template>
+<div class="cpu-memory">
+	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none">
+		<defs>
+			<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
+				<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
+				<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
+			</linearGradient>
+			<mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
+				<polygon
+					:points="cpuPolygonPoints"
+					fill="#fff"
+					fill-opacity="0.5"/>
+				<polyline
+					:points="cpuPolylinePoints"
+					fill="none"
+					stroke="#fff"
+					stroke-width="1"/>
+			</mask>
+		</defs>
+		<rect
+			x="-1" y="-1"
+			:width="viewBoxX + 2" :height="viewBoxY + 2"
+			:style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/>
+		<text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text>
+	</svg>
+	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none">
+		<defs>
+			<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
+				<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
+				<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
+			</linearGradient>
+			<mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
+				<polygon
+					:points="memPolygonPoints"
+					fill="#fff"
+					fill-opacity="0.5"/>
+				<polyline
+					:points="memPolylinePoints"
+					fill="none"
+					stroke="#fff"
+					stroke-width="1"/>
+			</mask>
+		</defs>
+		<rect
+			x="-1" y="-1"
+			:width="viewBoxX + 2" :height="viewBoxY + 2"
+			:style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/>
+		<text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text>
+	</svg>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import uuid from 'uuid';
+
+export default Vue.extend({
+	props: ['connection'],
+	data() {
+		return {
+			viewBoxX: 50,
+			viewBoxY: 30,
+			stats: [],
+			cpuGradientId: uuid(),
+			cpuMaskId: uuid(),
+			memGradientId: uuid(),
+			memMaskId: uuid(),
+			cpuPolylinePoints: '',
+			memPolylinePoints: '',
+			cpuPolygonPoints: '',
+			memPolygonPoints: '',
+			cpuP: '',
+			memP: ''
+		};
+	},
+	mounted() {
+		this.connection.on('stats', this.onStats);
+	},
+	beforeDestroy() {
+		this.connection.off('stats', this.onStats);
+	},
+	methods: {
+		onStats(stats) {
+			stats.mem.used = stats.mem.total - stats.mem.free;
+			this.stats.push(stats);
+			if (this.stats.length > 50) this.stats.shift();
+
+			this.cpuPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - s.cpu_usage) * this.viewBoxY}`).join(' ');
+			this.memPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.mem.used / s.mem.total)) * this.viewBoxY}`).join(' ');
+
+			this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.cpuPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
+			this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.memPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
+
+			this.cpuP = (stats.cpu_usage * 100).toFixed(0);
+			this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.cpu-memory
+	> svg
+		display block
+		padding 10px
+		width 50%
+		float left
+
+		&:first-child
+			padding-right 5px
+
+		&:last-child
+			padding-left 5px
+
+		> text
+			font-size 5px
+			fill rgba(0, 0, 0, 0.55)
+
+			> tspan
+				opacity 0.5
+
+	&:after
+		content ""
+		display block
+		clear both
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/server.cpu.vue b/src/web/app/desktop/views/components/widgets/server.cpu.vue
new file mode 100644
index 000000000..337ff62ce
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/server.cpu.vue
@@ -0,0 +1,68 @@
+<template>
+<div class="cpu">
+	<x-pie class="pie" :value="usage"/>
+	<div>
+		<p>%fa:microchip%CPU</p>
+		<p>{{ cores }} Cores</p>
+		<p>{{ model }}</p>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XPie from './server.pie.vue';
+
+export default Vue.extend({
+	components: {
+		'x-pie': XPie
+	},
+	props: ['connection', 'meta'],
+	data() {
+		return {
+			usage: 0
+		};
+	},
+	mounted() {
+		this.connection.on('stats', this.onStats);
+	},
+	beforeDestroy() {
+		this.connection.off('stats', this.onStats);
+	},
+	methods: {
+		onStats(stats) {
+			this.usage = stats.cpu_usage;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.cpu
+	> .pie
+		padding 10px
+		height 100px
+		float left
+
+	> div
+		float left
+		width calc(100% - 100px)
+		padding 10px 10px 10px 0
+
+		> p
+			margin 0
+			font-size 12px
+			color #505050
+
+			&:first-child
+				font-weight bold
+
+				> [data-fa]
+					margin-right 4px
+
+	&:after
+		content ""
+		display block
+		clear both
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/server.disk.vue b/src/web/app/desktop/views/components/widgets/server.disk.vue
new file mode 100644
index 000000000..c21c56290
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/server.disk.vue
@@ -0,0 +1,76 @@
+<template>
+<div class="disk">
+	<x-pie class="pie" :value="usage"/>
+	<div>
+		<p>%fa:R hdd%Storage</p>
+		<p>Total: {{ total | bytes(1) }}</p>
+		<p>Available: {{ available | bytes(1) }}</p>
+		<p>Used: {{ used | bytes(1) }}</p>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XPie from './server.pie.vue';
+
+export default Vue.extend({
+	components: {
+		'x-pie': XPie
+	},
+	props: ['connection'],
+	data() {
+		return {
+			usage: 0,
+			total: 0,
+			used: 0,
+			available: 0
+		};
+	},
+	mounted() {
+		this.connection.on('stats', this.onStats);
+	},
+	beforeDestroy() {
+		this.connection.off('stats', this.onStats);
+	},
+	methods: {
+		onStats(stats) {
+			stats.disk.used = stats.disk.total - stats.disk.free;
+			this.usage = stats.disk.used / stats.disk.total;
+			this.total = stats.disk.total;
+			this.used = stats.disk.used;
+			this.available = stats.disk.available;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.disk
+	> .pie
+		padding 10px
+		height 100px
+		float left
+
+	> div
+		float left
+		width calc(100% - 100px)
+		padding 10px 10px 10px 0
+
+		> p
+			margin 0
+			font-size 12px
+			color #505050
+
+			&:first-child
+				font-weight bold
+
+				> [data-fa]
+					margin-right 4px
+
+	&:after
+		content ""
+		display block
+		clear both
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/server.info.vue b/src/web/app/desktop/views/components/widgets/server.info.vue
new file mode 100644
index 000000000..870baf149
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/server.info.vue
@@ -0,0 +1,25 @@
+<template>
+<div class="info">
+	<p>Maintainer: <b>{{ meta.maintainer }}</b></p>
+	<p>Machine: {{ meta.machine }}</p>
+	<p>Node: {{ meta.node }}</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['meta']
+});
+</script>
+
+<style lang="info" scoped>
+.uptimes
+	padding 10px 14px
+
+	> p
+		margin 0
+		font-size 12px
+		color #505050
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/server.memory.vue b/src/web/app/desktop/views/components/widgets/server.memory.vue
new file mode 100644
index 000000000..2afc627fd
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/server.memory.vue
@@ -0,0 +1,76 @@
+<template>
+<div class="memory">
+	<x-pie class="pie" :value="usage"/>
+	<div>
+		<p>%fa:flask%Memory</p>
+		<p>Total: {{ total | bytes(1) }}</p>
+		<p>Used: {{ used | bytes(1) }}</p>
+		<p>Free: {{ free | bytes(1) }}</p>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XPie from './server.pie.vue';
+
+export default Vue.extend({
+	components: {
+		'x-pie': XPie
+	},
+	props: ['connection'],
+	data() {
+		return {
+			usage: 0,
+			total: 0,
+			used: 0,
+			free: 0
+		};
+	},
+	mounted() {
+		this.connection.on('stats', this.onStats);
+	},
+	beforeDestroy() {
+		this.connection.off('stats', this.onStats);
+	},
+	methods: {
+		onStats(stats) {
+			stats.mem.used = stats.mem.total - stats.mem.free;
+			this.usage = stats.mem.used / stats.mem.total;
+			this.total = stats.mem.total;
+			this.used = stats.mem.used;
+			this.free = stats.mem.free;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.memory
+	> .pie
+		padding 10px
+		height 100px
+		float left
+
+	> div
+		float left
+		width calc(100% - 100px)
+		padding 10px 10px 10px 0
+
+		> p
+			margin 0
+			font-size 12px
+			color #505050
+
+			&:first-child
+				font-weight bold
+
+				> [data-fa]
+					margin-right 4px
+
+	&:after
+		content ""
+		display block
+		clear both
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/server.pie.vue b/src/web/app/desktop/views/components/widgets/server.pie.vue
new file mode 100644
index 000000000..45ca8101b
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/server.pie.vue
@@ -0,0 +1,61 @@
+<template>
+<svg viewBox="0 0 1 1" preserveAspectRatio="none">
+	<circle
+		:r="r"
+		cx="50%" cy="50%"
+		fill="none"
+		stroke-width="0.1"
+		stroke="rgba(0, 0, 0, 0.05)"/>
+	<circle
+		:r="r"
+		cx="50%" cy="50%"
+		:stroke-dasharray="Math.PI * (r * 2)"
+		:stroke-dashoffset="strokeDashoffset"
+		fill="none"
+		stroke-width="0.1"
+		:stroke="color"/>
+	<text x="50%" y="50%" dy="0.05" text-anchor="middle">{{ (p * 100).toFixed(0) }}%</text>
+</svg>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: {
+		value: {
+			type: Number,
+			required: true
+		}
+	},
+	data() {
+		return {
+			r: 0.4
+		};
+	},
+	computed: {
+		color(): string {
+			return `hsl(${180 - (this.value * 180)}, 80%, 70%)`;
+		},
+		strokeDashoffset(): number {
+			return (1 - this.value) * (Math.PI * (this.r * 2));
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+svg
+	display block
+	height 100%
+
+	> circle
+		transform-origin center
+		transform rotate(-90deg)
+		transition stroke-dashoffset 0.5s ease
+
+	> text
+		font-size 0.15px
+		fill rgba(0, 0, 0, 0.6)
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/server.uptimes.vue b/src/web/app/desktop/views/components/widgets/server.uptimes.vue
new file mode 100644
index 000000000..06713d83c
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/server.uptimes.vue
@@ -0,0 +1,46 @@
+<template>
+<div class="uptimes">
+	<p>Uptimes</p>
+	<p>Process: {{ process ? process.toFixed(0) : '---' }}s</p>
+	<p>OS: {{ os ? os.toFixed(0) : '---' }}s</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['connection'],
+	data() {
+		return {
+			process: 0,
+			os: 0
+		};
+	},
+	mounted() {
+		this.connection.on('stats', this.onStats);
+	},
+	beforeDestroy() {
+		this.connection.off('stats', this.onStats);
+	},
+	methods: {
+		onStats(stats) {
+			this.process = stats.process_uptime;
+			this.os = stats.os_uptime;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.uptimes
+	padding 10px 14px
+
+	> p
+		margin 0
+		font-size 12px
+		color #505050
+
+		&:first-child
+			font-weight bold
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/server.vue b/src/web/app/desktop/views/components/widgets/server.vue
new file mode 100644
index 000000000..5aa01fd4e
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/server.vue
@@ -0,0 +1,127 @@
+<template>
+<div class="mkw-server" :data-melt="props.design == 2">
+	<template v-if="props.design == 0">
+		<p class="title">%fa:server%%i18n:desktop.tags.mk-server-home-widget.title%</p>
+		<button @click="toggle" title="%i18n:desktop.tags.mk-server-home-widget.toggle%">%fa:sort%</button>
+	</template>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<template v-if="!fetching">
+		<x-cpu-memory v-show="props.view == 0" :connection="connection"/>
+		<x-cpu v-show="props.view == 1" :connection="connection" :meta="meta"/>
+		<x-memory v-show="props.view == 2" :connection="connection"/>
+		<x-disk v-show="props.view == 3" :connection="connection"/>
+		<x-uptimes v-show="props.view == 4" :connection="connection"/>
+		<x-info v-show="props.view == 5" :connection="connection" :meta="meta"/>
+	</template>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+import XCpuMemory from './server.cpu-memory.vue';
+import XCpu from './server.cpu.vue';
+import XMemory from './server.memory.vue';
+import XDisk from './server.disk.vue';
+import XUptimes from './server.uptimes.vue';
+import XInfo from './server.info.vue';
+
+export default define({
+	name: 'server',
+	props: {
+		design: 0,
+		view: 0
+	}
+}).extend({
+	components: {
+		'x-cpu-and-memory': XCpuMemory,
+		'x-cpu': XCpu,
+		'x-memory': XMemory,
+		'x-disk': XDisk,
+		'x-uptimes': XUptimes,
+		'x-info': XInfo
+	},
+	data() {
+		return {
+			fetching: true,
+			meta: null,
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		(this as any).os.getMeta().then(meta => {
+			this.meta = meta;
+			this.fetching = false;
+		});
+
+		this.connection = (this as any).os.streams.serverStream.getConnection();
+		this.connectionId = (this as any).os.streams.serverStream.use();
+	},
+	beforeDestroy() {
+		(this as any).os.streams.serverStream.dispose(this.connectionId);
+	},
+	methods: {
+		toggle() {
+			if (this.props.design == 5) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		},
+		func() {
+			this.toggle();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-server
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	&[data-melt]
+		background transparent !important
+		border none !important
+
+	> .title
+		z-index 1
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> [data-fa]
+			margin-right 4px
+
+	> button
+		position absolute
+		z-index 2
+		top 0
+		right 0
+		padding 0
+		width 42px
+		font-size 0.9em
+		line-height 42px
+		color #ccc
+
+		&:hover
+			color #aaa
+
+		&:active
+			color #999
+
+	> .fetching
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+</style>
diff --git a/src/web/app/filters/bytes.ts b/src/web/app/filters/bytes.ts
new file mode 100644
index 000000000..3afb11e9a
--- /dev/null
+++ b/src/web/app/filters/bytes.ts
@@ -0,0 +1,8 @@
+import Vue from 'vue';
+
+Vue.filter('bytes', (v, digits = 0) => {
+	const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+	if (v == 0) return '0Byte';
+	const i = Math.floor(Math.log(v) / Math.log(1024));
+	return (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
+});
diff --git a/src/web/app/filters/index.ts b/src/web/app/filters/index.ts
new file mode 100644
index 000000000..16ff8c87a
--- /dev/null
+++ b/src/web/app/filters/index.ts
@@ -0,0 +1 @@
+require('./bytes');
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 8abb7f7aa..c3eede0d3 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -20,6 +20,9 @@ require('./common/views/directives');
 // Register global components
 require('./common/views/components');
 
+// Register global filters
+require('./filters');
+
 Vue.mixin({
 	destroyed(this: any) {
 		if (this.$el.parentNode) {

From ccf153b8cd8b12f9500d57fac87a211450ff8bdb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 07:58:21 +0900
Subject: [PATCH 206/286] wip

---
 .../{context-menu-menu.vue => context-menu.menu.vue}          | 4 ++--
 src/web/app/desktop/views/components/context-menu.vue         | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)
 rename src/web/app/desktop/views/components/{context-menu-menu.vue => context-menu.menu.vue} (98%)

diff --git a/src/web/app/desktop/views/components/context-menu-menu.vue b/src/web/app/desktop/views/components/context-menu.menu.vue
similarity index 98%
rename from src/web/app/desktop/views/components/context-menu-menu.vue
rename to src/web/app/desktop/views/components/context-menu.menu.vue
index 7e333d273..317833d9a 100644
--- a/src/web/app/desktop/views/components/context-menu-menu.vue
+++ b/src/web/app/desktop/views/components/context-menu.menu.vue
@@ -1,5 +1,5 @@
 <template>
-<ul class="me-nu">
+<ul class="menu">
 	<li v-for="(item, i) in menu" :key="i" :class="item.type">
 		<template v-if="item.type == 'item'">
 			<p @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</p>
@@ -29,7 +29,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.me-nu
+.menu
 	$width = 240px
 	$item-height = 38px
 	$padding = 10px
diff --git a/src/web/app/desktop/views/components/context-menu.vue b/src/web/app/desktop/views/components/context-menu.vue
index 3ba475e11..6076cdeb9 100644
--- a/src/web/app/desktop/views/components/context-menu.vue
+++ b/src/web/app/desktop/views/components/context-menu.vue
@@ -8,7 +8,7 @@
 import Vue from 'vue';
 import * as anime from 'animejs';
 import contains from '../../../common/scripts/contains';
-import meNu from './context-menu-menu.vue';
+import meNu from './context-menu.menu.vue';
 
 export default Vue.extend({
 	components: {

From 56fcf15cae5366a27f1f9acb2d29db7170eeca67 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 08:05:41 +0900
Subject: [PATCH 207/286] wip

---
 src/web/app/desktop/views/components/drive-file.vue | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/web/app/desktop/views/components/drive-file.vue b/src/web/app/desktop/views/components/drive-file.vue
index 772b9baf5..ffdf7ef57 100644
--- a/src/web/app/desktop/views/components/drive-file.vue
+++ b/src/web/app/desktop/views/components/drive-file.vue
@@ -30,7 +30,6 @@ import Vue from 'vue';
 import * as anime from 'animejs';
 import contextmenu from '../../api/contextmenu';
 import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
-import bytesToSize from '../../../common/scripts/bytes-to-size';
 
 export default Vue.extend({
 	props: ['file'],
@@ -48,7 +47,7 @@ export default Vue.extend({
 			return this.browser.selectedFiles.some(f => f.id == this.file.id);
 		},
 		title(): string {
-			return `${this.file.name}\n${this.file.type} ${bytesToSize(this.file.datasize)}`;
+			return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`;
 		},
 		background(): string {
 			return this.file.properties.average_color

From 58af12ccc3806fc438733bb43ee3cf1422324b2b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 08:14:44 +0900
Subject: [PATCH 208/286] wip

---
 .../app/desktop/-tags/big-follow-button.tag   | 153 ------------------
 .../views/components/follow-button.vue        | 116 +++++++------
 2 files changed, 65 insertions(+), 204 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/big-follow-button.tag

diff --git a/src/web/app/desktop/-tags/big-follow-button.tag b/src/web/app/desktop/-tags/big-follow-button.tag
deleted file mode 100644
index d8222f92c..000000000
--- a/src/web/app/desktop/-tags/big-follow-button.tag
+++ /dev/null
@@ -1,153 +0,0 @@
-<mk-big-follow-button>
-	<button :class="{ wait: wait, follow: !user.is_following, unfollow: user.is_following }" v-if="!init" @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
-		<span v-if="!wait && user.is_following">%fa:minus%フォロー解除</span>
-		<span v-if="!wait && !user.is_following">%fa:plus%フォロー</span>
-		<template v-if="wait">%fa:spinner .pulse .fw%</template>
-	</button>
-	<div class="init" v-if="init">%fa:spinner .pulse .fw%</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> button
-			> .init
-				display block
-				cursor pointer
-				padding 0
-				margin 0
-				width 100%
-				line-height 38px
-				font-size 1em
-				outline none
-				border-radius 4px
-
-				*
-					pointer-events none
-
-				i
-					margin-right 8px
-
-				&:focus
-					&:after
-						content ""
-						pointer-events none
-						position absolute
-						top -5px
-						right -5px
-						bottom -5px
-						left -5px
-						border 2px solid rgba($theme-color, 0.3)
-						border-radius 8px
-
-				&.follow
-					color #888
-					background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
-					border solid 1px #e2e2e2
-
-					&:hover
-						background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
-						border-color #dcdcdc
-
-					&:active
-						background #ececec
-						border-color #dcdcdc
-
-				&.unfollow
-					color $theme-color-foreground
-					background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
-					border solid 1px lighten($theme-color, 15%)
-
-					&:not(:disabled)
-						font-weight bold
-
-					&:hover:not(:disabled)
-						background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
-						border-color $theme-color
-
-					&:active:not(:disabled)
-						background $theme-color
-						border-color $theme-color
-
-				&.wait
-					cursor wait !important
-					opacity 0.7
-
-	</style>
-	<script lang="typescript">
-		import isPromise from '../../common/scripts/is-promise';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.user = null;
-		this.userPromise = isPromise(this.opts.user)
-			? this.opts.user
-			: Promise.resolve(this.opts.user);
-		this.init = true;
-		this.wait = false;
-
-		this.on('mount', () => {
-			this.userPromise.then(user => {
-				this.update({
-					init: false,
-					user: user
-				});
-				this.connection.on('follow', this.onStreamFollow);
-				this.connection.on('unfollow', this.onStreamUnfollow);
-			});
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('follow', this.onStreamFollow);
-			this.connection.off('unfollow', this.onStreamUnfollow);
-			this.stream.dispose(this.connectionId);
-		});
-
-		this.onStreamFollow = user => {
-			if (user.id == this.user.id) {
-				this.update({
-					user: user
-				});
-			}
-		};
-
-		this.onStreamUnfollow = user => {
-			if (user.id == this.user.id) {
-				this.update({
-					user: user
-				});
-			}
-		};
-
-		this.onclick = () => {
-			this.wait = true;
-			if (this.user.is_following) {
-				this.$root.$data.os.api('following/delete', {
-					user_id: this.user.id
-				}).then(() => {
-					this.user.is_following = false;
-				}).catch(err => {
-					console.error(err);
-				}).then(() => {
-					this.wait = false;
-					this.update();
-				});
-			} else {
-				this.$root.$data.os.api('following/create', {
-					user_id: this.user.id
-				}).then(() => {
-					this.user.is_following = true;
-				}).catch(err => {
-					console.error(err);
-				}).then(() => {
-					this.wait = false;
-					this.update();
-				});
-			}
-		};
-	</script>
-</mk-big-follow-button>
diff --git a/src/web/app/desktop/views/components/follow-button.vue b/src/web/app/desktop/views/components/follow-button.vue
index 4697fb05e..9056307bb 100644
--- a/src/web/app/desktop/views/components/follow-button.vue
+++ b/src/web/app/desktop/views/components/follow-button.vue
@@ -1,12 +1,18 @@
 <template>
 <button class="mk-follow-button"
-	:class="{ wait, follow: !user.is_following, unfollow: user.is_following }"
+	:class="{ wait, follow: !user.is_following, unfollow: user.is_following, big: size == 'big' }"
 	@click="onClick"
 	:disabled="wait"
 	:title="user.is_following ? 'フォロー解除' : 'フォローする'"
 >
-	<template v-if="!wait && user.is_following">%fa:minus%</template>
-	<template v-if="!wait && !user.is_following">%fa:plus%</template>
+	<template v-if="!wait && user.is_following">
+		<template v-if="size == 'compact'">%fa:minus%</template>
+		<template v-if="size == 'big'">%fa:minus%フォロー解除</template>
+	</template>
+	<template v-if="!wait && !user.is_following">
+		<template v-if="size == 'compact'">%fa:plus%</template>
+		<template v-if="size == 'big'">%fa:plus%フォロー</template>
+	</template>
 	<template v-if="wait">%fa:spinner .pulse .fw%</template>
 </button>
 </template>
@@ -18,6 +24,10 @@ export default Vue.extend({
 		user: {
 			type: Object,
 			required: true
+		},
+		size: {
+			type: String,
+			default: 'compact'
 		}
 	},
 	data() {
@@ -84,65 +94,69 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 .mk-follow-button
 	display block
+	cursor pointer
+	padding 0
+	margin 0
+	width 32px
+	height 32px
+	font-size 1em
+	outline none
+	border-radius 4px
 
-	> button
-	> .init
-		display block
-		cursor pointer
-		padding 0
-		margin 0
-		width 32px
-		height 32px
-		font-size 1em
-		outline none
-		border-radius 4px
+	*
+		pointer-events none
 
-		*
+	&:focus
+		&:after
+			content ""
 			pointer-events none
+			position absolute
+			top -5px
+			right -5px
+			bottom -5px
+			left -5px
+			border 2px solid rgba($theme-color, 0.3)
+			border-radius 8px
 
-		&:focus
-			&:after
-				content ""
-				pointer-events none
-				position absolute
-				top -5px
-				right -5px
-				bottom -5px
-				left -5px
-				border 2px solid rgba($theme-color, 0.3)
-				border-radius 8px
+	&.follow
+		color #888
+		background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
+		border solid 1px #e2e2e2
 
-		&.follow
-			color #888
-			background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
-			border solid 1px #e2e2e2
+		&:hover
+			background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
+			border-color #dcdcdc
 
-			&:hover
-				background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
-				border-color #dcdcdc
+		&:active
+			background #ececec
+			border-color #dcdcdc
 
-			&:active
-				background #ececec
-				border-color #dcdcdc
+	&.unfollow
+		color $theme-color-foreground
+		background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+		border solid 1px lighten($theme-color, 15%)
 
-		&.unfollow
-			color $theme-color-foreground
-			background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
-			border solid 1px lighten($theme-color, 15%)
+		&:not(:disabled)
+			font-weight bold
 
-			&:not(:disabled)
-				font-weight bold
+		&:hover:not(:disabled)
+			background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
+			border-color $theme-color
 
-			&:hover:not(:disabled)
-				background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
-				border-color $theme-color
+		&:active:not(:disabled)
+			background $theme-color
+			border-color $theme-color
 
-			&:active:not(:disabled)
-				background $theme-color
-				border-color $theme-color
+	&.wait
+		cursor wait !important
+		opacity 0.7
 
-		&.wait
-			cursor wait !important
-			opacity 0.7
+	&.big
+		width 100%
+		height 38px
+		line-height 38px
+
+		i
+			margin-right 8px
 
 </style>

From 45c55604cf162bc78332cf79d445784f20eb37bc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 09:25:47 +0900
Subject: [PATCH 209/286] wip

---
 .../app/desktop/-tags/home-widgets/trends.tag | 125 -----------------
 .../views/components/widgets/trends.vue       | 128 ++++++++++++++++++
 2 files changed, 128 insertions(+), 125 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/trends.tag
 create mode 100644 src/web/app/desktop/views/components/widgets/trends.vue

diff --git a/src/web/app/desktop/-tags/home-widgets/trends.tag b/src/web/app/desktop/-tags/home-widgets/trends.tag
deleted file mode 100644
index 9f1be68c7..000000000
--- a/src/web/app/desktop/-tags/home-widgets/trends.tag
+++ /dev/null
@@ -1,125 +0,0 @@
-<mk-trends-home-widget>
-	<template v-if="!data.compact">
-		<p class="title">%fa:fire%%i18n:desktop.tags.mk-trends-home-widget.title%</p>
-		<button @click="fetch" title="%i18n:desktop.tags.mk-trends-home-widget.refresh%">%fa:sync%</button>
-	</template>
-	<div class="post" v-if="!loading && post != null">
-		<p class="text"><a href="/{ post.user.username }/{ post.id }">{ post.text }</a></p>
-		<p class="author">―<a href="/{ post.user.username }">@{ post.user.username }</a></p>
-	</div>
-	<p class="empty" v-if="!loading && post == null">%i18n:desktop.tags.mk-trends-home-widget.nothing%</p>
-	<p class="loading" v-if="loading">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> .title
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				border-bottom solid 1px #eee
-
-				> [data-fa]
-					margin-right 4px
-
-			> button
-				position absolute
-				z-index 2
-				top 0
-				right 0
-				padding 0
-				width 42px
-				font-size 0.9em
-				line-height 42px
-				color #ccc
-
-				&:hover
-					color #aaa
-
-				&:active
-					color #999
-
-			> .post
-				padding 16px
-				font-size 12px
-				font-style oblique
-				color #555
-
-				> p
-					margin 0
-
-				> .text,
-				> .author
-					> a
-						color inherit
-
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-			> .loading
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			compact: false
-		};
-
-		this.mixin('widget');
-
-		this.post = null;
-		this.loading = true;
-
-		this.offset = 0;
-
-		this.on('mount', () => {
-			this.fetch();
-		});
-
-		this.fetch = () => {
-			this.update({
-				loading: true,
-				post: null
-			});
-			this.$root.$data.os.api('posts/trend', {
-				limit: 1,
-				offset: this.offset,
-				repost: false,
-				reply: false,
-				media: false,
-				poll: false
-			}).then(posts => {
-				const post = posts ? posts[0] : null;
-				if (post == null) {
-					this.offset = 0;
-				} else {
-					this.offset++;
-				}
-				this.update({
-					loading: false,
-					post: post
-				});
-			});
-		};
-
-		this.func = () => {
-			this.data.compact = !this.data.compact;
-			this.save();
-		};
-	</script>
-</mk-trends-home-widget>
diff --git a/src/web/app/desktop/views/components/widgets/trends.vue b/src/web/app/desktop/views/components/widgets/trends.vue
new file mode 100644
index 000000000..23d39563f
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/trends.vue
@@ -0,0 +1,128 @@
+<template>
+<div class="mkw-trends">
+	<template v-if="!data.compact">
+		<p class="title">%fa:fire%%i18n:desktop.tags.mk-trends-home-widget.title%</p>
+		<button @click="fetch" title="%i18n:desktop.tags.mk-trends-home-widget.refresh%">%fa:sync%</button>
+	</template>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<div class="post" v-else-if="post != null">
+		<p class="text"><a href="/{ post.user.username }/{ post.id }">{ post.text }</a></p>
+		<p class="author">―<a href="/{ post.user.username }">@{ post.user.username }</a></p>
+	</div>
+	<p class="empty" v-else>%i18n:desktop.tags.mk-trends-home-widget.nothing%</p>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+export default define({
+	name: 'trends',
+	props: {
+		compact: false
+	}
+}).extend({
+	data() {
+		return {
+			post: null,
+			fetching: true,
+			offset: 0
+		};
+	},
+	mounted() {
+		this.fetch();
+	},
+	methods: {
+		func() {
+			this.props.compact = !this.props.compact;
+		},
+		fetch() {
+			this.fetching = true;
+			this.post = null;
+
+			(this as any).api('posts/trend', {
+				limit: 1,
+				offset: this.offset,
+				repost: false,
+				reply: false,
+				media: false,
+				poll: false
+			}).then(posts => {
+				const post = posts ? posts[0] : null;
+				if (post == null) {
+					this.offset = 0;
+				} else {
+					this.offset++;
+				}
+				this.post = post;
+				this.fetching = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-trends
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	> .title
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		border-bottom solid 1px #eee
+
+		> [data-fa]
+			margin-right 4px
+
+	> button
+		position absolute
+		z-index 2
+		top 0
+		right 0
+		padding 0
+		width 42px
+		font-size 0.9em
+		line-height 42px
+		color #ccc
+
+		&:hover
+			color #aaa
+
+		&:active
+			color #999
+
+	> .post
+		padding 16px
+		font-size 12px
+		font-style oblique
+		color #555
+
+		> p
+			margin 0
+
+		> .text,
+		> .author
+			> a
+				color inherit
+
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+	> .fetching
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+</style>

From e707c3d5e3908c78e7ffcfda2c91ff3eda4c5954 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 09:48:30 +0900
Subject: [PATCH 210/286] wip

---
 .../desktop/-tags/home-widgets/mentions.tag   | 125 ------------------
 .../app/desktop/views/components/mentions.vue | 123 +++++++++++++++++
 .../app/desktop/views/components/timeline.vue |   4 +-
 3 files changed, 125 insertions(+), 127 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/mentions.tag
 create mode 100644 src/web/app/desktop/views/components/mentions.vue

diff --git a/src/web/app/desktop/-tags/home-widgets/mentions.tag b/src/web/app/desktop/-tags/home-widgets/mentions.tag
deleted file mode 100644
index d38ccabb5..000000000
--- a/src/web/app/desktop/-tags/home-widgets/mentions.tag
+++ /dev/null
@@ -1,125 +0,0 @@
-<mk-mentions-home-widget>
-	<header><span data-is-active={ mode == 'all' } @click="setMode.bind(this, 'all')">すべて</span><span data-is-active={ mode == 'following' } @click="setMode.bind(this, 'following')">フォロー中</span></header>
-	<div class="loading" v-if="isLoading">
-		<mk-ellipsis-icon/>
-	</div>
-	<p class="empty" v-if="isEmpty">%fa:R comments%<span v-if="mode == 'all'">あなた宛ての投稿はありません。</span><span v-if="mode == 'following'">あなたがフォローしているユーザーからの言及はありません。</span></p>
-	<mk-timeline ref="timeline">
-		<yield to="footer">
-			<template v-if="!parent.moreLoading">%fa:moon%</template>
-			<template v-if="parent.moreLoading">%fa:spinner .pulse .fw%</template>
-		</yield/>
-	</mk-timeline>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> header
-				padding 8px 16px
-				border-bottom solid 1px #eee
-
-				> span
-					margin-right 16px
-					line-height 27px
-					font-size 18px
-					color #555
-
-					&:not([data-is-active])
-						color $theme-color
-						cursor pointer
-
-						&:hover
-							text-decoration underline
-
-			> .loading
-				padding 64px 0
-
-			> .empty
-				display block
-				margin 0 auto
-				padding 32px
-				max-width 400px
-				text-align center
-				color #999
-
-				> [data-fa]
-					display block
-					margin-bottom 16px
-					font-size 3em
-					color #ccc
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-		this.mixin('api');
-
-		this.isLoading = true;
-		this.isEmpty = false;
-		this.moreLoading = false;
-		this.mode = 'all';
-
-		this.on('mount', () => {
-			document.addEventListener('keydown', this.onDocumentKeydown);
-			window.addEventListener('scroll', this.onScroll);
-
-			this.fetch(() => this.$emit('loaded'));
-		});
-
-		this.on('unmount', () => {
-			document.removeEventListener('keydown', this.onDocumentKeydown);
-			window.removeEventListener('scroll', this.onScroll);
-		});
-
-		this.onDocumentKeydown = e => {
-			if (e.target.tagName != 'INPUT' && tag != 'TEXTAREA') {
-				if (e.which == 84) { // t
-					this.$refs.timeline.focus();
-				}
-			}
-		};
-
-		this.fetch = cb => {
-			this.$root.$data.os.api('posts/mentions', {
-				following: this.mode == 'following'
-			}).then(posts => {
-				this.update({
-					isLoading: false,
-					isEmpty: posts.length == 0
-				});
-				this.$refs.timeline.setPosts(posts);
-				if (cb) cb();
-			});
-		};
-
-		this.more = () => {
-			if (this.moreLoading || this.isLoading || this.$refs.timeline.posts.length == 0) return;
-			this.update({
-				moreLoading: true
-			});
-			this.$root.$data.os.api('posts/mentions', {
-				following: this.mode == 'following',
-				until_id: this.$refs.timeline.tail().id
-			}).then(posts => {
-				this.update({
-					moreLoading: false
-				});
-				this.$refs.timeline.prependPosts(posts);
-			});
-		};
-
-		this.onScroll = () => {
-			const current = window.scrollY + window.innerHeight;
-			if (current > document.body.offsetHeight - 8) this.more();
-		};
-
-		this.setMode = mode => {
-			this.update({
-				mode: mode
-			});
-			this.fetch();
-		};
-	</script>
-</mk-mentions-home-widget>
diff --git a/src/web/app/desktop/views/components/mentions.vue b/src/web/app/desktop/views/components/mentions.vue
new file mode 100644
index 000000000..28ba59f2b
--- /dev/null
+++ b/src/web/app/desktop/views/components/mentions.vue
@@ -0,0 +1,123 @@
+<template>
+<div class="mk-mentions">
+	<header>
+		<span :data-is-active="mode == 'all'" @click="mode = 'all'">すべて</span>
+		<span :data-is-active="mode == 'following'" @click="mode = 'following'">フォロー中</span>
+	</header>
+	<div class="fetching" v-if="fetching">
+		<mk-ellipsis-icon/>
+	</div>
+	<p class="empty" v-if="posts.length == 0 && !fetching">
+		%fa:R comments%
+		<span v-if="mode == 'all'">あなた宛ての投稿はありません。</span>
+		<span v-if="mode == 'following'">あなたがフォローしているユーザーからの言及はありません。</span>
+	</p>
+	<mk-posts :posts="posts" ref="timeline"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			fetching: true,
+			moreFetching: false,
+			mode: 'all',
+			posts: []
+		};
+	},
+	watch: {
+		mode() {
+			this.fetch();
+		}
+	},
+	mounted() {
+		document.addEventListener('keydown', this.onDocumentKeydown);
+		window.addEventListener('scroll', this.onScroll);
+
+		this.fetch(() => this.$emit('loaded'));
+	},
+	beforeDestroy() {
+		document.removeEventListener('keydown', this.onDocumentKeydown);
+		window.removeEventListener('scroll', this.onScroll);
+	},
+	methods: {
+		onDocumentKeydown(e) {
+			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
+				if (e.which == 84) { // t
+					(this.$refs.timeline as any).focus();
+				}
+			}
+		},
+		onScroll() {
+			const current = window.scrollY + window.innerHeight;
+			if (current > document.body.offsetHeight - 8) this.more();
+		},
+		fetch(cb?) {
+			this.fetching = true;
+			this.posts =  [];
+			(this as any).api('posts/mentions', {
+				following: this.mode == 'following'
+			}).then(posts => {
+				this.posts = posts;
+				this.fetching = false;
+				if (cb) cb();
+			});
+		},
+		more() {
+			if (this.moreFetching || this.fetching || this.posts.length == 0) return;
+			this.moreFetching = true;
+			(this as any).api('posts/mentions', {
+				following: this.mode == 'following',
+				until_id: this.posts[this.posts.length - 1].id
+			}).then(posts => {
+				this.posts = this.posts.concat(posts);
+				this.moreFetching = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-mentions
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	> header
+		padding 8px 16px
+		border-bottom solid 1px #eee
+
+		> span
+			margin-right 16px
+			line-height 27px
+			font-size 18px
+			color #555
+
+			&:not([data-is-active])
+				color $theme-color
+				cursor pointer
+
+				&:hover
+					text-decoration underline
+
+	> .fetching
+		padding 64px 0
+
+	> .empty
+		display block
+		margin 0 auto
+		padding 32px
+		max-width 400px
+		text-align center
+		color #999
+
+		> [data-fa]
+			display block
+			margin-bottom 16px
+			font-size 3em
+			color #ccc
+
+</style>
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index 3e0677475..875a7961e 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-timeline">
 	<mk-friends-maker v-if="alone"/>
-	<div class="loading" v-if="fetching">
+	<div class="fetching" v-if="fetching">
 		<mk-ellipsis-icon/>
 	</div>
 	<p class="empty" v-if="posts.length == 0 && !fetching">%fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。</p>
@@ -106,7 +106,7 @@ export default Vue.extend({
 	> .mk-following-setuper
 		border-bottom solid 1px #eee
 
-	> .loading
+	> .fetching
 		padding 64px 0
 
 	> .empty

From 533f36aac32f4a82bd1ba29f5420edf2ef2c66bd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 10:15:18 +0900
Subject: [PATCH 211/286] wip

---
 src/web/app/mobile/tags/drive/file.tag        | 151 ----------------
 src/web/app/mobile/tags/drive/folder.tag      |  53 ------
 .../mobile/views/components/drive.file.vue    | 169 ++++++++++++++++++
 .../mobile/views/components/drive.folder.vue  |  58 ++++++
 4 files changed, 227 insertions(+), 204 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/drive/file.tag
 delete mode 100644 src/web/app/mobile/tags/drive/folder.tag
 create mode 100644 src/web/app/mobile/views/components/drive.file.vue
 create mode 100644 src/web/app/mobile/views/components/drive.folder.vue

diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag
deleted file mode 100644
index 8afac7982..000000000
--- a/src/web/app/mobile/tags/drive/file.tag
+++ /dev/null
@@ -1,151 +0,0 @@
-<mk-drive-file data-is-selected={ isSelected }>
-	<a @click="onclick" href="/i/drive/file/{ file.id }">
-		<div class="container">
-			<div class="thumbnail" style={ thumbnail }></div>
-			<div class="body">
-				<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" v-if="file.name.lastIndexOf('.') != -1">{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
-				<!--
-				if file.tags.length > 0
-					ul.tags
-						each tag in file.tags
-							li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name
-				-->
-				<footer>
-					<p class="type"><mk-file-type-icon type={ file.type }/>{ file.type }</p>
-					<p class="separator"></p>
-					<p class="data-size">{ bytesToSize(file.datasize) }</p>
-					<p class="separator"></p>
-					<p class="created-at">
-						%fa:R clock%<mk-time time={ file.created_at }/>
-					</p>
-				</footer>
-			</div>
-		</div>
-	</a>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> a
-				display block
-				text-decoration none !important
-
-				*
-					user-select none
-					pointer-events none
-
-				> .container
-					max-width 500px
-					margin 0 auto
-					padding 16px
-
-					&:after
-						content ""
-						display block
-						clear both
-
-					> .thumbnail
-						display block
-						float left
-						width 64px
-						height 64px
-						background-size cover
-						background-position center center
-
-					> .body
-						display block
-						float left
-						width calc(100% - 74px)
-						margin-left 10px
-
-						> .name
-							display block
-							margin 0
-							padding 0
-							font-size 0.9em
-							font-weight bold
-							color #555
-							text-overflow ellipsis
-							overflow-wrap break-word
-
-							> .ext
-								opacity 0.5
-
-						> .tags
-							display block
-							margin 4px 0 0 0
-							padding 0
-							list-style none
-							font-size 0.5em
-
-							> .tag
-								display inline-block
-								margin 0 5px 0 0
-								padding 1px 5px
-								border-radius 2px
-
-						> footer
-							display block
-							margin 4px 0 0 0
-							font-size 0.7em
-
-							> .separator
-								display inline
-								margin 0
-								padding 0 4px
-								color #CDCDCD
-
-							> .type
-								display inline
-								margin 0
-								padding 0
-								color #9D9D9D
-
-								> mk-file-type-icon
-									margin-right 4px
-
-							> .data-size
-								display inline
-								margin 0
-								padding 0
-								color #9D9D9D
-
-							> .created-at
-								display inline
-								margin 0
-								padding 0
-								color #BDBDBD
-
-								> [data-fa]
-									margin-right 2px
-
-			&[data-is-selected]
-				background $theme-color
-
-				&, *
-					color #fff !important
-
-	</style>
-	<script lang="typescript">
-		import bytesToSize from '../../../common/scripts/bytes-to-size';
-		this.bytesToSize = bytesToSize;
-
-		this.browser = this.parent;
-		this.file = this.opts.file;
-		this.thumbnail = {
-			'background-color': this.file.properties.average_color ? `rgb(${this.file.properties.average_color.join(',')})` : 'transparent',
-			'background-image': `url(${this.file.url}?thumbnail&size=128)`
-		};
-		this.isSelected = this.browser.selectedFiles.some(f => f.id == this.file.id);
-
-		this.browser.on('change-selection', selections => {
-			this.isSelected = selections.some(f => f.id == this.file.id);
-		});
-
-		this.onclick = ev => {
-			ev.preventDefault();
-			this.browser.chooseFile(this.file);
-			return false;
-		};
-	</script>
-</mk-drive-file>
diff --git a/src/web/app/mobile/tags/drive/folder.tag b/src/web/app/mobile/tags/drive/folder.tag
deleted file mode 100644
index 2fe6c2c39..000000000
--- a/src/web/app/mobile/tags/drive/folder.tag
+++ /dev/null
@@ -1,53 +0,0 @@
-<mk-drive-folder>
-	<a @click="onclick" href="/i/drive/folder/{ folder.id }">
-		<div class="container">
-			<p class="name">%fa:folder%{ folder.name }</p>%fa:angle-right%
-		</div>
-	</a>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> a
-				display block
-				color #777
-				text-decoration none !important
-
-				*
-					user-select none
-					pointer-events none
-
-				> .container
-					max-width 500px
-					margin 0 auto
-					padding 16px
-
-					> .name
-						display block
-						margin 0
-						padding 0
-
-						> [data-fa]
-							margin-right 6px
-
-					> [data-fa]
-						position absolute
-						top 0
-						bottom 0
-						right 20px
-
-						> *
-							height 100%
-
-	</style>
-	<script lang="typescript">
-		this.browser = this.parent;
-		this.folder = this.opts.folder;
-
-		this.onclick = ev => {
-			ev.preventDefault();
-			this.browser.cd(this.folder);
-			return false;
-		};
-	</script>
-</mk-drive-folder>
diff --git a/src/web/app/mobile/views/components/drive.file.vue b/src/web/app/mobile/views/components/drive.file.vue
new file mode 100644
index 000000000..dfc69e249
--- /dev/null
+++ b/src/web/app/mobile/views/components/drive.file.vue
@@ -0,0 +1,169 @@
+<template>
+<a class="file" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected">
+	<div class="container">
+		<div class="thumbnail" :style="thumbnail"></div>
+		<div class="body">
+			<p class="name">
+				<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
+				<span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
+			</p>
+			<!--
+			if file.tags.length > 0
+				ul.tags
+					each tag in file.tags
+						li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name
+			-->
+			<footer>
+				<p class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</p>
+				<p class="separator"></p>
+				<p class="data-size">{{ file.datasize | bytes }}</p>
+				<p class="separator"></p>
+				<p class="created-at">
+					%fa:R clock%<mk-time :time="file.created_at"/>
+				</p>
+			</footer>
+		</div>
+	</div>
+</a>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['file'],
+	data() {
+		return {
+			isSelected: false
+		};
+	},
+	computed: {
+		browser(): any {
+			return this.$parent;
+		},
+		thumbnail(): any {
+			return {
+				'background-color': this.file.properties.average_color ? `rgb(${this.file.properties.average_color.join(',')})` : 'transparent',
+				'background-image': `url(${this.file.url}?thumbnail&size=128)`
+			};
+		}
+	},
+	created() {
+		this.isSelected = this.browser.selectedFiles.some(f => f.id == this.file.id)
+
+		this.browser.$on('change-selection', this.onBrowserChangeSelection);
+	},
+	beforeDestroy() {
+		this.browser.$off('change-selection', this.onBrowserChangeSelection);
+	},
+	methods: {
+		onBrowserChangeSelection(selections) {
+			this.isSelected = selections.some(f => f.id == this.file.id);
+		},
+		onClick() {
+			this.browser.chooseFile(this.file);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.file
+	display block
+	text-decoration none !important
+
+	*
+		user-select none
+		pointer-events none
+
+	> .container
+		max-width 500px
+		margin 0 auto
+		padding 16px
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		> .thumbnail
+			display block
+			float left
+			width 64px
+			height 64px
+			background-size cover
+			background-position center center
+
+		> .body
+			display block
+			float left
+			width calc(100% - 74px)
+			margin-left 10px
+
+			> .name
+				display block
+				margin 0
+				padding 0
+				font-size 0.9em
+				font-weight bold
+				color #555
+				text-overflow ellipsis
+				overflow-wrap break-word
+
+				> .ext
+					opacity 0.5
+
+			> .tags
+				display block
+				margin 4px 0 0 0
+				padding 0
+				list-style none
+				font-size 0.5em
+
+				> .tag
+					display inline-block
+					margin 0 5px 0 0
+					padding 1px 5px
+					border-radius 2px
+
+			> footer
+				display block
+				margin 4px 0 0 0
+				font-size 0.7em
+
+				> .separator
+					display inline
+					margin 0
+					padding 0 4px
+					color #CDCDCD
+
+				> .type
+					display inline
+					margin 0
+					padding 0
+					color #9D9D9D
+
+					> mk-file-type-icon
+						margin-right 4px
+
+				> .data-size
+					display inline
+					margin 0
+					padding 0
+					color #9D9D9D
+
+				> .created-at
+					display inline
+					margin 0
+					padding 0
+					color #BDBDBD
+
+					> [data-fa]
+						margin-right 2px
+
+	&[data-is-selected]
+		background $theme-color
+
+		&, *
+			color #fff !important
+
+</style>
diff --git a/src/web/app/mobile/views/components/drive.folder.vue b/src/web/app/mobile/views/components/drive.folder.vue
new file mode 100644
index 000000000..b776af7aa
--- /dev/null
+++ b/src/web/app/mobile/views/components/drive.folder.vue
@@ -0,0 +1,58 @@
+<template>
+<a class="folder" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`">
+	<div class="container">
+		<p class="name">%fa:folder%{{ folder.name }}</p>%fa:angle-right%
+	</div>
+</a>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['folder'],
+	computed: {
+		browser(): any {
+			return this.$parent;
+		}
+	},
+	methods: {
+		onClick() {
+			this.browser.cd(this.folder);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.folder
+	display block
+	color #777
+	text-decoration none !important
+
+	*
+		user-select none
+		pointer-events none
+
+	> .container
+		max-width 500px
+		margin 0 auto
+		padding 16px
+
+		> .name
+			display block
+			margin 0
+			padding 0
+
+			> [data-fa]
+				margin-right 6px
+
+		> [data-fa]
+			position absolute
+			top 0
+			bottom 0
+			right 20px
+
+			> *
+				height 100%
+
+</style>

From 161a91a69f2cf4a640243c0869a724dcfd414d48 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 12:38:45 +0900
Subject: [PATCH 212/286] wip

---
 .eslintrc                                     |   3 +-
 src/web/app/mobile/tags/drive/file-viewer.tag | 282 -----------------
 .../views/components/drive.file-detail.vue    | 290 ++++++++++++++++++
 src/web/app/mobile/views/components/drive.vue |   2 +-
 4 files changed, 293 insertions(+), 284 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/drive/file-viewer.tag
 create mode 100644 src/web/app/mobile/views/components/drive.file-detail.vue

diff --git a/.eslintrc b/.eslintrc
index d30cf2aa5..6caf8f532 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -12,6 +12,7 @@
 		"vue/html-indent": false,
 		"vue/html-self-closing": false,
 		"vue/no-unused-vars": false,
-		"no-console": 0
+		"no-console": 0,
+		"no-unused-vars": 0
 	}
 }
diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag
deleted file mode 100644
index e9a89493e..000000000
--- a/src/web/app/mobile/tags/drive/file-viewer.tag
+++ /dev/null
@@ -1,282 +0,0 @@
-<mk-drive-file-viewer>
-	<div class="preview">
-		<img v-if="kind == 'image'" ref="img"
-			src={ file.url }
-			alt={ file.name }
-			title={ file.name }
-			onload={ onImageLoaded }
-			style="background-color:rgb({ file.properties.average_color.join(',') })">
-		<template v-if="kind != 'image'">%fa:file%</template>
-		<footer v-if="kind == 'image' && file.properties && file.properties.width && file.properties.height">
-			<span class="size">
-				<span class="width">{ file.properties.width }</span>
-				<span class="time">×</span>
-				<span class="height">{ file.properties.height }</span>
-				<span class="px">px</span>
-			</span>
-			<span class="separator"></span>
-			<span class="aspect-ratio">
-				<span class="width">{ file.properties.width / gcd(file.properties.width, file.properties.height) }</span>
-				<span class="colon">:</span>
-				<span class="height">{ file.properties.height / gcd(file.properties.width, file.properties.height) }</span>
-			</span>
-		</footer>
-	</div>
-	<div class="info">
-		<div>
-			<span class="type"><mk-file-type-icon type={ file.type }/>{ file.type }</span>
-			<span class="separator"></span>
-			<span class="data-size">{ bytesToSize(file.datasize) }</span>
-			<span class="separator"></span>
-			<span class="created-at" @click="showCreatedAt">%fa:R clock%<mk-time time={ file.created_at }/></span>
-		</div>
-	</div>
-	<div class="menu">
-		<div>
-			<a href={ file.url + '?download' } download={ file.name }>
-				%fa:download%%i18n:mobile.tags.mk-drive-file-viewer.download%
-			</a>
-			<button @click="rename">
-				%fa:pencil-alt%%i18n:mobile.tags.mk-drive-file-viewer.rename%
-			</button>
-			<button @click="move">
-				%fa:R folder-open%%i18n:mobile.tags.mk-drive-file-viewer.move%
-			</button>
-		</div>
-	</div>
-	<div class="exif" show={ exif }>
-		<div>
-			<p>
-				%fa:camera%%i18n:mobile.tags.mk-drive-file-viewer.exif%
-			</p>
-			<pre ref="exif" class="json">{ exif ? JSON.stringify(exif, null, 2) : '' }</pre>
-		</div>
-	</div>
-	<div class="hash">
-		<div>
-			<p>
-				%fa:hashtag%%i18n:mobile.tags.mk-drive-file-viewer.hash%
-			</p>
-			<code>{ file.md5 }</code>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> .preview
-				padding 8px
-				background #f0f0f0
-
-				> img
-					display block
-					max-width 100%
-					max-height 300px
-					margin 0 auto
-					box-shadow 1px 1px 4px rgba(0, 0, 0, 0.2)
-
-				> footer
-					padding 8px 8px 0 8px
-					font-size 0.8em
-					color #888
-					text-align center
-
-					> .separator
-						display inline
-						padding 0 4px
-
-					> .size
-						display inline
-
-						.time
-							margin 0 2px
-
-						.px
-							margin-left 4px
-
-					> .aspect-ratio
-						display inline
-						opacity 0.7
-
-						&:before
-							content "("
-
-						&:after
-							content ")"
-
-			> .info
-				padding 14px
-				font-size 0.8em
-				border-top solid 1px #dfdfdf
-
-				> div
-					max-width 500px
-					margin 0 auto
-
-					> .separator
-						padding 0 4px
-						color #cdcdcd
-
-					> .type
-					> .data-size
-						color #9d9d9d
-
-						> mk-file-type-icon
-							margin-right 4px
-
-					> .created-at
-						color #bdbdbd
-
-						> [data-fa]
-							margin-right 2px
-
-			> .menu
-				padding 14px
-				border-top solid 1px #dfdfdf
-
-				> div
-					max-width 500px
-					margin 0 auto
-
-					> *
-						display block
-						width 100%
-						padding 10px 16px
-						margin 0 0 12px 0
-						color #333
-						font-size 0.9em
-						text-align center
-						text-decoration none
-						text-shadow 0 1px 0 rgba(255, 255, 255, 0.9)
-						background-image linear-gradient(#fafafa, #eaeaea)
-						border 1px solid #ddd
-						border-bottom-color #cecece
-						border-radius 3px
-
-						&:last-child
-							margin-bottom 0
-
-						&:active
-							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)
-
-						> [data-fa]
-							margin-right 4px
-
-			> .hash
-				padding 14px
-				border-top solid 1px #dfdfdf
-
-				> div
-					max-width 500px
-					margin 0 auto
-
-					> p
-						display block
-						margin 0
-						padding 0
-						color #555
-						font-size 0.9em
-
-						> [data-fa]
-							margin-right 4px
-
-					> code
-						display block
-						width 100%
-						margin 6px 0 0 0
-						padding 8px
-						white-space nowrap
-						overflow auto
-						font-size 0.8em
-						color #222
-						border solid 1px #dfdfdf
-						border-radius 2px
-						background #f5f5f5
-
-			> .exif
-				padding 14px
-				border-top solid 1px #dfdfdf
-
-				> div
-					max-width 500px
-					margin 0 auto
-
-					> p
-						display block
-						margin 0
-						padding 0
-						color #555
-						font-size 0.9em
-
-						> [data-fa]
-							margin-right 4px
-
-					> pre
-						display block
-						width 100%
-						margin 6px 0 0 0
-						padding 8px
-						height 128px
-						overflow auto
-						font-size 0.9em
-						border solid 1px #dfdfdf
-						border-radius 2px
-						background #f5f5f5
-
-	</style>
-	<script lang="typescript">
-		import EXIF from 'exif-js';
-		import hljs from 'highlight.js';
-		import bytesToSize from '../../../common/scripts/bytes-to-size';
-		import gcd from '../../../common/scripts/gcd';
-
-		this.bytesToSize = bytesToSize;
-		this.gcd = gcd;
-
-		this.mixin('api');
-
-		this.file = this.opts.file;
-		this.kind = this.file.type.split('/')[0];
-
-		this.onImageLoaded = () => {
-			const self = this;
-			EXIF.getData(this.$refs.img, function() {
-				const allMetaData = EXIF.getAllTags(this);
-				self.update({
-					exif: allMetaData
-				});
-				hljs.highlightBlock(self.refs.exif);
-			});
-		};
-
-		this.rename = () => {
-			const name = window.prompt('名前を変更', this.file.name);
-			if (name == null || name == '' || name == this.file.name) return;
-			this.$root.$data.os.api('drive/files/update', {
-				file_id: this.file.id,
-				name: name
-			}).then(() => {
-				this.parent.cf(this.file, true);
-			});
-		};
-
-		this.move = () => {
-			const dialog = riot.mount(document.body.appendChild(document.createElement('mk-drive-folder-selector')))[0];
-			dialog.one('selected', folder => {
-				this.$root.$data.os.api('drive/files/update', {
-					file_id: this.file.id,
-					folder_id: folder == null ? null : folder.id
-				}).then(() => {
-					this.parent.cf(this.file, true);
-				});
-			});
-		};
-
-		this.showCreatedAt = () => {
-			alert(new Date(this.file.created_at).toLocaleString());
-		};
-	</script>
-</mk-drive-file-viewer>
diff --git a/src/web/app/mobile/views/components/drive.file-detail.vue b/src/web/app/mobile/views/components/drive.file-detail.vue
new file mode 100644
index 000000000..db0c3c701
--- /dev/null
+++ b/src/web/app/mobile/views/components/drive.file-detail.vue
@@ -0,0 +1,290 @@
+<template>
+<div class="file-detail">
+	<div class="preview">
+		<img v-if="kind == 'image'" ref="img"
+			:src="file.url"
+			:alt="file.name"
+			:title="file.name"
+			@load="onImageLoaded"
+			:style="`background-color:rgb(${ file.properties.average_color.join(',') })`">
+		<template v-if="kind != 'image'">%fa:file%</template>
+		<footer v-if="kind == 'image' && file.properties && file.properties.width && file.properties.height">
+			<span class="size">
+				<span class="width">{{ file.properties.width }}</span>
+				<span class="time">×</span>
+				<span class="height">{{ file.properties.height }}</span>
+				<span class="px">px</span>
+			</span>
+			<span class="separator"></span>
+			<span class="aspect-ratio">
+				<span class="width">{{ file.properties.width / gcd(file.properties.width, file.properties.height) }}</span>
+				<span class="colon">:</span>
+				<span class="height">{{ file.properties.height / gcd(file.properties.width, file.properties.height) }}</span>
+			</span>
+		</footer>
+	</div>
+	<div class="info">
+		<div>
+			<span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span>
+			<span class="separator"></span>
+			<span class="data-size">{{ file.datasize | bytes }}</span>
+			<span class="separator"></span>
+			<span class="created-at" @click="showCreatedAt">%fa:R clock%<mk-time :time="file.created_at"/></span>
+		</div>
+	</div>
+	<div class="menu">
+		<div>
+			<a :href="`${file.url}?download`" :download="file.name">
+				%fa:download%%i18n:mobile.tags.mk-drive-file-viewer.download%
+			</a>
+			<button @click="rename">
+				%fa:pencil-alt%%i18n:mobile.tags.mk-drive-file-viewer.rename%
+			</button>
+			<button @click="move">
+				%fa:R folder-open%%i18n:mobile.tags.mk-drive-file-viewer.move%
+			</button>
+		</div>
+	</div>
+	<div class="exif" v-show="exif">
+		<div>
+			<p>
+				%fa:camera%%i18n:mobile.tags.mk-drive-file-viewer.exif%
+			</p>
+			<pre ref="exif" class="json">{{ exif ? JSON.stringify(exif, null, 2) : '' }}</pre>
+		</div>
+	</div>
+	<div class="hash">
+		<div>
+			<p>
+				%fa:hashtag%%i18n:mobile.tags.mk-drive-file-viewer.hash%
+			</p>
+			<code>{{ file.md5 }}</code>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import EXIF from 'exif-js';
+import hljs from 'highlight.js';
+import gcd from '../../../common/scripts/gcd';
+
+export default Vue.extend({
+	props: ['file'],
+	data() {
+		return {
+			gcd,
+			exif: null
+		};
+	},
+	computed: {
+		browser(): any {
+			return this.$parent;
+		},
+		kind(): string {
+			return this.file.type.split('/')[0];
+		}
+	},
+	methods: {
+		rename() {
+			const name = window.prompt('名前を変更', this.file.name);
+			if (name == null || name == '' || name == this.file.name) return;
+			(this as any).api('drive/files/update', {
+				file_id: this.file.id,
+				name: name
+			}).then(() => {
+				this.browser.cf(this.file, true);
+			});
+		},
+		move() {
+			(this as any).apis.chooseDriveFolder().then(folder => {
+				(this as any).api('drive/files/update', {
+					file_id: this.file.id,
+					folder_id: folder == null ? null : folder.id
+				}).then(() => {
+					this.browser.cf(this.file, true);
+				});
+			});
+		},
+		showCreatedAt() {
+			alert(new Date(this.file.created_at).toLocaleString());
+		},
+		onImageLoaded() {
+			const self = this;
+			EXIF.getData(this.$refs.img, function(this: any) {
+				const allMetaData = EXIF.getAllTags(this);
+				self.exif = allMetaData;
+				hljs.highlightBlock(self.$refs.exif);
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.file-detail
+
+	> .preview
+		padding 8px
+		background #f0f0f0
+
+		> img
+			display block
+			max-width 100%
+			max-height 300px
+			margin 0 auto
+			box-shadow 1px 1px 4px rgba(0, 0, 0, 0.2)
+
+		> footer
+			padding 8px 8px 0 8px
+			font-size 0.8em
+			color #888
+			text-align center
+
+			> .separator
+				display inline
+				padding 0 4px
+
+			> .size
+				display inline
+
+				.time
+					margin 0 2px
+
+				.px
+					margin-left 4px
+
+			> .aspect-ratio
+				display inline
+				opacity 0.7
+
+				&:before
+					content "("
+
+				&:after
+					content ")"
+
+	> .info
+		padding 14px
+		font-size 0.8em
+		border-top solid 1px #dfdfdf
+
+		> div
+			max-width 500px
+			margin 0 auto
+
+			> .separator
+				padding 0 4px
+				color #cdcdcd
+
+			> .type
+			> .data-size
+				color #9d9d9d
+
+				> mk-file-type-icon
+					margin-right 4px
+
+			> .created-at
+				color #bdbdbd
+
+				> [data-fa]
+					margin-right 2px
+
+	> .menu
+		padding 14px
+		border-top solid 1px #dfdfdf
+
+		> div
+			max-width 500px
+			margin 0 auto
+
+			> *
+				display block
+				width 100%
+				padding 10px 16px
+				margin 0 0 12px 0
+				color #333
+				font-size 0.9em
+				text-align center
+				text-decoration none
+				text-shadow 0 1px 0 rgba(255, 255, 255, 0.9)
+				background-image linear-gradient(#fafafa, #eaeaea)
+				border 1px solid #ddd
+				border-bottom-color #cecece
+				border-radius 3px
+
+				&:last-child
+					margin-bottom 0
+
+				&:active
+					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)
+
+				> [data-fa]
+					margin-right 4px
+
+	> .hash
+		padding 14px
+		border-top solid 1px #dfdfdf
+
+		> div
+			max-width 500px
+			margin 0 auto
+
+			> p
+				display block
+				margin 0
+				padding 0
+				color #555
+				font-size 0.9em
+
+				> [data-fa]
+					margin-right 4px
+
+			> code
+				display block
+				width 100%
+				margin 6px 0 0 0
+				padding 8px
+				white-space nowrap
+				overflow auto
+				font-size 0.8em
+				color #222
+				border solid 1px #dfdfdf
+				border-radius 2px
+				background #f5f5f5
+
+	> .exif
+		padding 14px
+		border-top solid 1px #dfdfdf
+
+		> div
+			max-width 500px
+			margin 0 auto
+
+			> p
+				display block
+				margin 0
+				padding 0
+				color #555
+				font-size 0.9em
+
+				> [data-fa]
+					margin-right 4px
+
+			> pre
+				display block
+				width 100%
+				margin 6px 0 0 0
+				padding 8px
+				height 128px
+				overflow auto
+				font-size 0.9em
+				border solid 1px #dfdfdf
+				border-radius 2px
+				background #f5f5f5
+
+</style>
diff --git a/src/web/app/mobile/views/components/drive.vue b/src/web/app/mobile/views/components/drive.vue
index 0e5456332..59b2c256d 100644
--- a/src/web/app/mobile/views/components/drive.vue
+++ b/src/web/app/mobile/views/components/drive.vue
@@ -47,7 +47,7 @@
 		</div>
 	</div>
 	<input ref="file" type="file" multiple="multiple" @change="onChangeLocalFile"/>
-	<mk-drive-file-viewer v-if="file != null" :file="file"/>
+	<mk-drive-file-detail v-if="file != null" :file="file"/>
 </div>
 </template>
 

From dfa98a038c9146cade8c0863315bd8dd7038b6ef Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 13:02:53 +0900
Subject: [PATCH 213/286] wip

---
 src/web/app/mobile/tags/page/settings.tag     | 100 -------
 .../app/mobile/tags/page/settings/profile.tag | 247 ------------------
 .../mobile/views/pages/profile-setting.vue    | 218 ++++++++++++++++
 src/web/app/mobile/views/pages/settings.vue   | 102 ++++++++
 4 files changed, 320 insertions(+), 347 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/page/settings.tag
 delete mode 100644 src/web/app/mobile/tags/page/settings/profile.tag
 create mode 100644 src/web/app/mobile/views/pages/profile-setting.vue
 create mode 100644 src/web/app/mobile/views/pages/settings.vue

diff --git a/src/web/app/mobile/tags/page/settings.tag b/src/web/app/mobile/tags/page/settings.tag
deleted file mode 100644
index 394c198b0..000000000
--- a/src/web/app/mobile/tags/page/settings.tag
+++ /dev/null
@@ -1,100 +0,0 @@
-<mk-settings-page>
-	<mk-ui ref="ui">
-		<mk-settings />
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import ui from '../../scripts/ui-event';
-
-		this.on('mount', () => {
-			document.title = 'Misskey | %i18n:mobile.tags.mk-settings-page.settings%';
-			ui.trigger('title', '%fa:cog%%i18n:mobile.tags.mk-settings-page.settings%');
-			document.documentElement.style.background = '#313a42';
-		});
-	</script>
-</mk-settings-page>
-
-<mk-settings>
-	<p><mk-raw content={ '%i18n:mobile.tags.mk-settings.signed-in-as%'.replace('{}', '<b>' + I.name + '</b>') }/></p>
-	<ul>
-		<li><a href="./settings/profile">%fa:user%%i18n:mobile.tags.mk-settings-page.profile%%fa:angle-right%</a></li>
-		<li><a href="./settings/authorized-apps">%fa:puzzle-piece%%i18n:mobile.tags.mk-settings-page.applications%%fa:angle-right%</a></li>
-		<li><a href="./settings/twitter">%fa:B twitter%%i18n:mobile.tags.mk-settings-page.twitter-integration%%fa:angle-right%</a></li>
-		<li><a href="./settings/signin-history">%fa:sign-in-alt%%i18n:mobile.tags.mk-settings-page.signin-history%%fa:angle-right%</a></li>
-	</ul>
-	<ul>
-		<li><a @click="signout">%fa:power-off%%i18n:mobile.tags.mk-settings-page.signout%</a></li>
-	</ul>
-	<p><small>ver { _VERSION_ } (葵 aoi)</small></p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> p
-				display block
-				margin 24px
-				text-align center
-				color #cad2da
-
-			> ul
-				$radius = 8px
-
-				display block
-				margin 16px auto
-				padding 0
-				max-width 500px
-				width calc(100% - 32px)
-				list-style none
-				background #fff
-				border solid 1px rgba(0, 0, 0, 0.2)
-				border-radius $radius
-
-				> li
-					display block
-					border-bottom solid 1px #ddd
-
-					&:hover
-						background rgba(0, 0, 0, 0.1)
-
-					&:first-child
-						border-top-left-radius $radius
-						border-top-right-radius $radius
-
-					&:last-child
-						border-bottom-left-radius $radius
-						border-bottom-right-radius $radius
-						border-bottom none
-
-					> a
-						$height = 48px
-
-						display block
-						position relative
-						padding 0 16px
-						line-height $height
-						color #4d635e
-
-						> [data-fa]:nth-of-type(1)
-							margin-right 4px
-
-						> [data-fa]:nth-of-type(2)
-							display block
-							position absolute
-							top 0
-							right 8px
-							z-index 1
-							padding 0 20px
-							font-size 1.2em
-							line-height $height
-
-	</style>
-	<script lang="typescript">
-		import signout from '../../../common/scripts/signout';
-		this.signout = signout;
-
-		this.mixin('i');
-	</script>
-</mk-settings>
diff --git a/src/web/app/mobile/tags/page/settings/profile.tag b/src/web/app/mobile/tags/page/settings/profile.tag
deleted file mode 100644
index 6f7ef3ac3..000000000
--- a/src/web/app/mobile/tags/page/settings/profile.tag
+++ /dev/null
@@ -1,247 +0,0 @@
-<mk-profile-setting-page>
-	<mk-ui ref="ui">
-		<mk-profile-setting/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import ui from '../../../scripts/ui-event';
-
-		this.on('mount', () => {
-			document.title = 'Misskey | %i18n:mobile.tags.mk-profile-setting-page.title%';
-			ui.trigger('title', '%fa:user%%i18n:mobile.tags.mk-profile-setting-page.title%');
-			document.documentElement.style.background = '#313a42';
-		});
-	</script>
-</mk-profile-setting-page>
-
-<mk-profile-setting>
-	<div>
-		<p>%fa:info-circle%%i18n:mobile.tags.mk-profile-setting.will-be-published%</p>
-		<div class="form">
-			<div style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=1024)' : '' } @click="clickBanner">
-				<img src={ I.avatar_url + '?thumbnail&size=200' } alt="avatar" @click="clickAvatar"/>
-			</div>
-			<label>
-				<p>%i18n:mobile.tags.mk-profile-setting.name%</p>
-				<input ref="name" type="text" value={ I.name }/>
-			</label>
-			<label>
-				<p>%i18n:mobile.tags.mk-profile-setting.location%</p>
-				<input ref="location" type="text" value={ I.profile.location }/>
-			</label>
-			<label>
-				<p>%i18n:mobile.tags.mk-profile-setting.description%</p>
-				<textarea ref="description">{ I.description }</textarea>
-			</label>
-			<label>
-				<p>%i18n:mobile.tags.mk-profile-setting.birthday%</p>
-				<input ref="birthday" type="date" value={ I.profile.birthday }/>
-			</label>
-			<label>
-				<p>%i18n:mobile.tags.mk-profile-setting.avatar%</p>
-				<button @click="setAvatar" disabled={ avatarSaving }>%i18n:mobile.tags.mk-profile-setting.set-avatar%</button>
-			</label>
-			<label>
-				<p>%i18n:mobile.tags.mk-profile-setting.banner%</p>
-				<button @click="setBanner" disabled={ bannerSaving }>%i18n:mobile.tags.mk-profile-setting.set-banner%</button>
-			</label>
-		</div>
-		<button class="save" @click="save" disabled={ saving }>%fa:check%%i18n:mobile.tags.mk-profile-setting.save%</button>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> div
-				margin 8px auto
-				max-width 500px
-				width calc(100% - 16px)
-
-				@media (min-width 500px)
-					margin 16px auto
-					width calc(100% - 32px)
-
-				> p
-					display block
-					margin 0 0 8px 0
-					padding 12px 16px
-					font-size 14px
-					color #79d4e6
-					border solid 1px #71afbb
-					//color #276f86
-					//background #f8ffff
-					//border solid 1px #a9d5de
-					border-radius 8px
-
-					> [data-fa]
-						margin-right 6px
-
-				> .form
-					position relative
-					background #fff
-					box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
-					border-radius 8px
-
-					&:before
-						content ""
-						display block
-						position absolute
-						bottom -20px
-						left calc(50% - 10px)
-						border-top solid 10px rgba(0, 0, 0, 0.2)
-						border-right solid 10px transparent
-						border-bottom solid 10px transparent
-						border-left solid 10px transparent
-
-					&:after
-						content ""
-						display block
-						position absolute
-						bottom -16px
-						left calc(50% - 8px)
-						border-top solid 8px #fff
-						border-right solid 8px transparent
-						border-bottom solid 8px transparent
-						border-left solid 8px transparent
-
-					> div
-						height 128px
-						background-color #e4e4e4
-						background-size cover
-						background-position center
-						border-radius 8px 8px 0 0
-
-						> img
-							position absolute
-							top 25px
-							left calc(50% - 40px)
-							width 80px
-							height 80px
-							border solid 2px #fff
-							border-radius 8px
-
-					> label
-						display block
-						margin 0
-						padding 16px
-						border-bottom solid 1px #eee
-
-						&:last-of-type
-							border none
-
-						> p:first-child
-							display block
-							margin 0
-							padding 0 0 4px 0
-							font-weight bold
-							color #2f3c42
-
-						> input[type="text"]
-						> textarea
-							display block
-							width 100%
-							padding 12px
-							font-size 16px
-							color #192427
-							border solid 2px #ddd
-							border-radius 4px
-
-						> textarea
-							min-height 80px
-
-				> .save
-					display block
-					margin 8px 0 0 0
-					padding 16px
-					width 100%
-					font-size 16px
-					color $theme-color-foreground
-					background $theme-color
-					border-radius 8px
-
-					&:disabled
-						opacity 0.7
-
-					> [data-fa]
-						margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-		this.mixin('api');
-
-		this.setAvatar = () => {
-			const i = riot.mount(document.body.appendChild(document.createElement('mk-drive-selector')), {
-				multiple: false
-			})[0];
-			i.one('selected', file => {
-				this.update({
-					avatarSaving: true
-				});
-
-				this.$root.$data.os.api('i/update', {
-					avatar_id: file.id
-				}).then(() => {
-					this.update({
-						avatarSaving: false
-					});
-
-					alert('%i18n:mobile.tags.mk-profile-setting.avatar-saved%');
-				});
-			});
-		};
-
-		this.setBanner = () => {
-			const i = riot.mount(document.body.appendChild(document.createElement('mk-drive-selector')), {
-				multiple: false
-			})[0];
-			i.one('selected', file => {
-				this.update({
-					bannerSaving: true
-				});
-
-				this.$root.$data.os.api('i/update', {
-					banner_id: file.id
-				}).then(() => {
-					this.update({
-						bannerSaving: false
-					});
-
-					alert('%i18n:mobile.tags.mk-profile-setting.banner-saved%');
-				});
-			});
-		};
-
-		this.clickAvatar = e => {
-			this.setAvatar();
-			return false;
-		};
-
-		this.clickBanner = e => {
-			this.setBanner();
-			return false;
-		};
-
-		this.save = () => {
-			this.update({
-				saving: true
-			});
-
-			this.$root.$data.os.api('i/update', {
-				name: this.$refs.name.value,
-				location: this.$refs.location.value || null,
-				description: this.$refs.description.value || null,
-				birthday: this.$refs.birthday.value || null
-			}).then(() => {
-				this.update({
-					saving: false
-				});
-
-				alert('%i18n:mobile.tags.mk-profile-setting.saved%');
-			});
-		};
-	</script>
-</mk-profile-setting>
diff --git a/src/web/app/mobile/views/pages/profile-setting.vue b/src/web/app/mobile/views/pages/profile-setting.vue
new file mode 100644
index 000000000..3b93496a3
--- /dev/null
+++ b/src/web/app/mobile/views/pages/profile-setting.vue
@@ -0,0 +1,218 @@
+<template>
+<mk-ui>
+	<span slot="header">%fa:user%%i18n:mobile.tags.mk-profile-setting-page.title%</span>
+	<div class="$style.content">
+		<p>%fa:info-circle%%i18n:mobile.tags.mk-profile-setting.will-be-published%</p>
+		<div class="$style.form">
+			<div :style="os.i.banner_url ? `background-image: url(${os.i.banner_url}?thumbnail&size=1024)` : ''" @click="setBanner">
+				<img :src="`${os.i.avatar_url}?thumbnail&size=200`" alt="avatar" @click="setAvatar"/>
+			</div>
+			<label>
+				<p>%i18n:mobile.tags.mk-profile-setting.name%</p>
+				<input v-model="name" type="text"/>
+			</label>
+			<label>
+				<p>%i18n:mobile.tags.mk-profile-setting.location%</p>
+				<input v-model="location" type="text"/>
+			</label>
+			<label>
+				<p>%i18n:mobile.tags.mk-profile-setting.description%</p>
+				<textarea v-model="description"></textarea>
+			</label>
+			<label>
+				<p>%i18n:mobile.tags.mk-profile-setting.birthday%</p>
+				<input v-model="birthday" type="date"/>
+			</label>
+			<label>
+				<p>%i18n:mobile.tags.mk-profile-setting.avatar%</p>
+				<button @click="setAvatar" :disabled="avatarSaving">%i18n:mobile.tags.mk-profile-setting.set-avatar%</button>
+			</label>
+			<label>
+				<p>%i18n:mobile.tags.mk-profile-setting.banner%</p>
+				<button @click="setBanner" :disabled="bannerSaving">%i18n:mobile.tags.mk-profile-setting.set-banner%</button>
+			</label>
+		</div>
+		<button class="$style.save" @click="save" :disabled="saving">%fa:check%%i18n:mobile.tags.mk-profile-setting.save%</button>
+	</div>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			name: (this as any).os.i.name,
+			location: (this as any).os.i.profile.location,
+			description: (this as any).os.i.description,
+			birthday: (this as any).os.i.profile.birthday,
+			avatarSaving: false,
+			bannerSaving: false,
+			saving: false
+		};
+	},
+	mounted() {
+		document.title = 'Misskey | %i18n:mobile.tags.mk-profile-setting-page.title%';
+		document.documentElement.style.background = '#313a42';
+	},
+	methods: {
+		setAvatar() {
+			(this as any).apis.chooseDriveFile({
+				multiple: false
+			}).then(file => {
+				this.avatarSaving = true;
+
+				(this as any).api('i/update', {
+					avatar_id: file.id
+				}).then(() => {
+					this.avatarSaving = false;
+					alert('%i18n:mobile.tags.mk-profile-setting.avatar-saved%');
+				});
+			});
+		},
+		setBanner() {
+			(this as any).apis.chooseDriveFile({
+				multiple: false
+			}).then(file => {
+				this.bannerSaving = true;
+
+				(this as any).api('i/update', {
+					banner_id: file.id
+				}).then(() => {
+					this.bannerSaving = false;
+					alert('%i18n:mobile.tags.mk-profile-setting.banner-saved%');
+				});
+			});
+		},
+		save() {
+			this.saving = true;
+
+			(this as any).api('i/update', {
+				name: this.name,
+				location: this.location || null,
+				description: this.description || null,
+				birthday: this.birthday || null
+			}).then(() => {
+				this.saving = false;
+				alert('%i18n:mobile.tags.mk-profile-setting.saved%');
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.content
+	margin 8px auto
+	max-width 500px
+	width calc(100% - 16px)
+
+	@media (min-width 500px)
+		margin 16px auto
+		width calc(100% - 32px)
+
+	> p
+		display block
+		margin 0 0 8px 0
+		padding 12px 16px
+		font-size 14px
+		color #79d4e6
+		border solid 1px #71afbb
+		//color #276f86
+		//background #f8ffff
+		//border solid 1px #a9d5de
+		border-radius 8px
+
+		> [data-fa]
+			margin-right 6px
+
+.form
+	position relative
+	background #fff
+	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+	border-radius 8px
+
+	&:before
+		content ""
+		display block
+		position absolute
+		bottom -20px
+		left calc(50% - 10px)
+		border-top solid 10px rgba(0, 0, 0, 0.2)
+		border-right solid 10px transparent
+		border-bottom solid 10px transparent
+		border-left solid 10px transparent
+
+	&:after
+		content ""
+		display block
+		position absolute
+		bottom -16px
+		left calc(50% - 8px)
+		border-top solid 8px #fff
+		border-right solid 8px transparent
+		border-bottom solid 8px transparent
+		border-left solid 8px transparent
+
+	> div
+		height 128px
+		background-color #e4e4e4
+		background-size cover
+		background-position center
+		border-radius 8px 8px 0 0
+
+		> img
+			position absolute
+			top 25px
+			left calc(50% - 40px)
+			width 80px
+			height 80px
+			border solid 2px #fff
+			border-radius 8px
+
+	> label
+		display block
+		margin 0
+		padding 16px
+		border-bottom solid 1px #eee
+
+		&:last-of-type
+			border none
+
+		> p:first-child
+			display block
+			margin 0
+			padding 0 0 4px 0
+			font-weight bold
+			color #2f3c42
+
+		> input[type="text"]
+		> textarea
+			display block
+			width 100%
+			padding 12px
+			font-size 16px
+			color #192427
+			border solid 2px #ddd
+			border-radius 4px
+
+		> textarea
+			min-height 80px
+
+.save
+	display block
+	margin 8px 0 0 0
+	padding 16px
+	width 100%
+	font-size 16px
+	color $theme-color-foreground
+	background $theme-color
+	border-radius 8px
+
+	&:disabled
+		opacity 0.7
+
+	> [data-fa]
+		margin-right 4px
+
+</style>
diff --git a/src/web/app/mobile/views/pages/settings.vue b/src/web/app/mobile/views/pages/settings.vue
new file mode 100644
index 000000000..a3d5dd92e
--- /dev/null
+++ b/src/web/app/mobile/views/pages/settings.vue
@@ -0,0 +1,102 @@
+<template>
+<mk-ui>
+	<span slot="header">%fa:cog%%i18n:mobile.tags.mk-settings-page.settings%</span>
+	<div class="$style.content">
+		<p v-html="'%i18n:mobile.tags.mk-settings.signed-in-as%'.replace('{}', '<b>' + os.i.name + '</b>')"></p>
+		<ul>
+			<li><router-link to="./settings/profile">%fa:user%%i18n:mobile.tags.mk-settings-page.profile%%fa:angle-right%</a></li>
+			<li><router-link to="./settings/authorized-apps">%fa:puzzle-piece%%i18n:mobile.tags.mk-settings-page.applications%%fa:angle-right%</router-link></li>
+			<li><router-link to="./settings/twitter">%fa:B twitter%%i18n:mobile.tags.mk-settings-page.twitter-integration%%fa:angle-right%</router-link></li>
+			<li><router-link to="./settings/signin-history">%fa:sign-in-alt%%i18n:mobile.tags.mk-settings-page.signin-history%%fa:angle-right%</router-link></li>
+		</ul>
+		<ul>
+			<li><a @click="signout">%fa:power-off%%i18n:mobile.tags.mk-settings-page.signout%</a></li>
+		</ul>
+		<p><small>ver {{ v }} (葵 aoi)</small></p>
+	</div>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { version } from '../../../../config';
+
+export default Vue.extend({
+	data() {
+		return {
+			v: version
+		};
+	},
+	mounted() {
+		document.title = 'Misskey | %i18n:mobile.tags.mk-settings-page.settings%';
+		document.documentElement.style.background = '#313a42';
+	},
+	methods: {
+		signout() {
+			(this as any).os.signout();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.content
+
+	> p
+		display block
+		margin 24px
+		text-align center
+		color #cad2da
+
+	> ul
+		$radius = 8px
+
+		display block
+		margin 16px auto
+		padding 0
+		max-width 500px
+		width calc(100% - 32px)
+		list-style none
+		background #fff
+		border solid 1px rgba(0, 0, 0, 0.2)
+		border-radius $radius
+
+		> li
+			display block
+			border-bottom solid 1px #ddd
+
+			&:hover
+				background rgba(0, 0, 0, 0.1)
+
+			&:first-child
+				border-top-left-radius $radius
+				border-top-right-radius $radius
+
+			&:last-child
+				border-bottom-left-radius $radius
+				border-bottom-right-radius $radius
+				border-bottom none
+
+			> a
+				$height = 48px
+
+				display block
+				position relative
+				padding 0 16px
+				line-height $height
+				color #4d635e
+
+				> [data-fa]:nth-of-type(1)
+					margin-right 4px
+
+				> [data-fa]:nth-of-type(2)
+					display block
+					position absolute
+					top 0
+					right 8px
+					z-index 1
+					padding 0 20px
+					font-size 1.2em
+					line-height $height
+
+</style>

From eda7534e41c0e562af1acc3eceb24c2aac2b9667 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 13:58:25 +0900
Subject: [PATCH 214/286] wip

---
 src/web/app/mobile/tags/page/drive.tag   | 73 ---------------------
 src/web/app/mobile/views/pages/drive.vue | 83 ++++++++++++++++++++++++
 2 files changed, 83 insertions(+), 73 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/page/drive.tag
 create mode 100644 src/web/app/mobile/views/pages/drive.vue

diff --git a/src/web/app/mobile/tags/page/drive.tag b/src/web/app/mobile/tags/page/drive.tag
deleted file mode 100644
index 23185b14b..000000000
--- a/src/web/app/mobile/tags/page/drive.tag
+++ /dev/null
@@ -1,73 +0,0 @@
-<mk-drive-page>
-	<mk-ui ref="ui">
-		<mk-drive ref="browser" folder={ parent.opts.folder } file={ parent.opts.file } is-naked={ true } top={ 48 }/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import ui from '../../scripts/ui-event';
-		import Progress from '../../../common/scripts/loading';
-
-		this.on('mount', () => {
-			document.title = 'Misskey Drive';
-			ui.trigger('title', '%fa:cloud%%i18n:mobile.tags.mk-drive-page.drive%');
-
-			ui.trigger('func', () => {
-				this.$refs.ui.refs.browser.openContextMenu();
-			}, '%fa:ellipsis-h%');
-
-			this.$refs.ui.refs.browser.on('begin-fetch', () => {
-				Progress.start();
-			});
-
-			this.$refs.ui.refs.browser.on('fetched-mid', () => {
-				Progress.set(0.5);
-			});
-
-			this.$refs.ui.refs.browser.on('fetched', () => {
-				Progress.done();
-			});
-
-			this.$refs.ui.refs.browser.on('move-root', () => {
-				const title = 'Misskey Drive';
-
-				// Rewrite URL
-				history.pushState(null, title, '/i/drive');
-
-				document.title = title;
-				ui.trigger('title', '%fa:cloud%%i18n:mobile.tags.mk-drive-page.drive%');
-			});
-
-			this.$refs.ui.refs.browser.on('open-folder', (folder, silent) => {
-				const title = folder.name + ' | Misskey Drive';
-
-				if (!silent) {
-					// Rewrite URL
-					history.pushState(null, title, '/i/drive/folder/' + folder.id);
-				}
-
-				document.title = title;
-				// TODO: escape html characters in folder.name
-				ui.trigger('title', '%fa:R folder-open%' + folder.name);
-			});
-
-			this.$refs.ui.refs.browser.on('open-file', (file, silent) => {
-				const title = file.name + ' | Misskey Drive';
-
-				if (!silent) {
-					// Rewrite URL
-					history.pushState(null, title, '/i/drive/file/' + file.id);
-				}
-
-				document.title = title;
-				// TODO: escape html characters in file.name
-				ui.trigger('title', '<mk-file-type-icon class="icon"></mk-file-type-icon>' + file.name);
-				riot.mount('mk-file-type-icon', {
-					type: file.type
-				});
-			});
-		});
-	</script>
-</mk-drive-page>
diff --git a/src/web/app/mobile/views/pages/drive.vue b/src/web/app/mobile/views/pages/drive.vue
new file mode 100644
index 000000000..0032068b6
--- /dev/null
+++ b/src/web/app/mobile/views/pages/drive.vue
@@ -0,0 +1,83 @@
+<template>
+<mk-ui :func="fn" func-icon="%fa:ellipsis-h%">
+	<span slot="header">
+		<template v-if="folder">%fa:R folder-open%{{ folder.name }}</template>
+		<template v-if="file"><mk-file-type-icon class="icon"/>{{ file.name }}</template>
+		<template v-else>%fa:cloud%%i18n:mobile.tags.mk-drive-page.drive%</template>
+	</span>
+	<mk-drive
+		ref="browser"
+		:init-folder="folder"
+		:init-file="file"
+		is-naked
+		:top="48"
+		@begin-fetch="Progress.start()"
+		@fetched-mid="Progress.set(0.5);"
+		@fetched="Progress.done()"
+		@move-root="onMoveRoot"
+		@open-folder="onOpenFolder"
+		@open-file="onOpenFile"
+	/>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+	data() {
+		return {
+			Progress,
+			folder: null,
+			file: null
+		};
+	},
+	mounted() {
+		document.title = 'Misskey Drive';
+	},
+	methods: {
+		fn() {
+			(this.$refs as any).browser.openContextMenu();
+		},
+		onMoveRoot() {
+			const title = 'Misskey Drive';
+
+			// Rewrite URL
+			history.pushState(null, title, '/i/drive');
+
+			document.title = title;
+
+			this.file = null;
+			this.folder = null;
+		},
+		onOpenFolder(folder, silent) {
+			const title = folder.name + ' | Misskey Drive';
+
+			if (!silent) {
+				// Rewrite URL
+				history.pushState(null, title, '/i/drive/folder/' + folder.id);
+			}
+
+			document.title = title;
+
+			this.file = null;
+			this.folder = folder;
+		},
+		onOpenFile(file, silent) {
+			const title = file.name + ' | Misskey Drive';
+
+			if (!silent) {
+				// Rewrite URL
+				history.pushState(null, title, '/i/drive/file/' + file.id);
+			}
+
+			document.title = title;
+
+			this.file = file;
+			this.folder = null;
+		}
+	}
+});
+</script>
+

From 6426a6ed5b65ca95b0dfc084fca3133e94551ec2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 14:00:20 +0900
Subject: [PATCH 215/286] wip

---
 src/web/app/mobile/views/pages/drive.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/mobile/views/pages/drive.vue b/src/web/app/mobile/views/pages/drive.vue
index 0032068b6..c4c22448c 100644
--- a/src/web/app/mobile/views/pages/drive.vue
+++ b/src/web/app/mobile/views/pages/drive.vue
@@ -12,7 +12,7 @@
 		is-naked
 		:top="48"
 		@begin-fetch="Progress.start()"
-		@fetched-mid="Progress.set(0.5);"
+		@fetched-mid="Progress.set(0.5)"
 		@fetched="Progress.done()"
 		@move-root="onMoveRoot"
 		@open-folder="onOpenFolder"

From 99296bc6398874bb8e057a36400b5b03aedd3ed3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 14:16:41 +0900
Subject: [PATCH 216/286] wip

---
 .../app/mobile/tags/page/messaging-room.tag   | 31 ------------------
 src/web/app/mobile/tags/page/messaging.tag    | 23 -------------
 .../app/mobile/views/pages/messaging-room.vue | 32 +++++++++++++++++++
 src/web/app/mobile/views/pages/messaging.vue  | 21 ++++++++++++
 4 files changed, 53 insertions(+), 54 deletions(-)
 delete mode 100644 src/web/app/mobile/tags/page/messaging-room.tag
 delete mode 100644 src/web/app/mobile/tags/page/messaging.tag
 create mode 100644 src/web/app/mobile/views/pages/messaging-room.vue
 create mode 100644 src/web/app/mobile/views/pages/messaging.vue

diff --git a/src/web/app/mobile/tags/page/messaging-room.tag b/src/web/app/mobile/tags/page/messaging-room.tag
deleted file mode 100644
index 262ece07a..000000000
--- a/src/web/app/mobile/tags/page/messaging-room.tag
+++ /dev/null
@@ -1,31 +0,0 @@
-<mk-messaging-room-page>
-	<mk-ui ref="ui">
-		<mk-messaging-room v-if="!parent.fetching" user={ parent.user } is-naked={ true }/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import ui from '../../scripts/ui-event';
-
-		this.mixin('api');
-
-		this.fetching = true;
-
-		this.on('mount', () => {
-			this.$root.$data.os.api('users/show', {
-				username: this.opts.username
-			}).then(user => {
-				this.update({
-					fetching: false,
-					user: user
-				});
-
-				document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${user.name} | Misskey`;
-				// TODO: ユーザー名をエスケープ
-				ui.trigger('title', '%fa:R comments%' + user.name);
-			});
-		});
-	</script>
-</mk-messaging-room-page>
diff --git a/src/web/app/mobile/tags/page/messaging.tag b/src/web/app/mobile/tags/page/messaging.tag
deleted file mode 100644
index 62998c711..000000000
--- a/src/web/app/mobile/tags/page/messaging.tag
+++ /dev/null
@@ -1,23 +0,0 @@
-<mk-messaging-page>
-	<mk-ui ref="ui">
-		<mk-messaging ref="index"/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import ui from '../../scripts/ui-event';
-
-		this.mixin('page');
-
-		this.on('mount', () => {
-			document.title = 'Misskey | %i18n:mobile.tags.mk-messaging-page.message%';
-			ui.trigger('title', '%fa:R comments%%i18n:mobile.tags.mk-messaging-page.message%');
-
-			this.$refs.ui.refs.index.on('navigate-user', user => {
-				this.page('/i/messaging/' + user.username);
-			});
-		});
-	</script>
-</mk-messaging-page>
diff --git a/src/web/app/mobile/views/pages/messaging-room.vue b/src/web/app/mobile/views/pages/messaging-room.vue
new file mode 100644
index 000000000..671ede217
--- /dev/null
+++ b/src/web/app/mobile/views/pages/messaging-room.vue
@@ -0,0 +1,32 @@
+<template>
+<mk-ui>
+	<span slot="header">
+		<template v-if="user">%fa:R comments%{{ user.name }}</template>
+		<template v-else><mk-ellipsis/></template>
+	</span>
+	<mk-messaging-room v-if="!fetching" :user="user" is-naked/>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			fetching: true,
+			user: null
+		};
+	},
+	mounted() {
+		(this as any).api('users/show', {
+			username: (this as any).$route.params.user
+		}).then(user => {
+			this.user = user;
+			this.fetching = false;
+
+			document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${user.name} | Misskey`;
+		});
+	}
+});
+</script>
+
diff --git a/src/web/app/mobile/views/pages/messaging.vue b/src/web/app/mobile/views/pages/messaging.vue
new file mode 100644
index 000000000..607e44650
--- /dev/null
+++ b/src/web/app/mobile/views/pages/messaging.vue
@@ -0,0 +1,21 @@
+<template>
+<mk-ui>
+	<span slot="header">%fa:R comments%%i18n:mobile.tags.mk-messaging-page.message%</span>
+	<mk-messaging @navigate="navigate"/>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	mounted() {
+		document.title = 'Misskey | %i18n:mobile.tags.mk-messaging-page.message%';
+	},
+	methods: {
+		navigate(user) {
+			(this as any).$router.push(`/i/messaging/${user.username}`);
+		}
+	}
+});
+</script>
+

From f8bb47a60b7ed895a83bae3daf389e72b65e3239 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 20:29:52 +0900
Subject: [PATCH 217/286] wip

---
 .../desktop/-tags/autocomplete-suggestion.tag | 197 ------------------
 .../desktop/views/components/autocomplete.vue | 190 +++++++++++++++++
 2 files changed, 190 insertions(+), 197 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/autocomplete-suggestion.tag
 create mode 100644 src/web/app/desktop/views/components/autocomplete.vue

diff --git a/src/web/app/desktop/-tags/autocomplete-suggestion.tag b/src/web/app/desktop/-tags/autocomplete-suggestion.tag
deleted file mode 100644
index d3c3b6b35..000000000
--- a/src/web/app/desktop/-tags/autocomplete-suggestion.tag
+++ /dev/null
@@ -1,197 +0,0 @@
-<mk-autocomplete-suggestion>
-	<ol class="users" ref="users" v-if="users.length > 0">
-		<li each={ users } @click="parent.onClick" onkeydown={ parent.onKeydown } tabindex="-1">
-			<img class="avatar" src={ avatar_url + '?thumbnail&size=32' } alt=""/>
-			<span class="name">{ name }</span>
-			<span class="username">@{ username }</span>
-		</li>
-	</ol>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			position absolute
-			z-index 65535
-			margin-top calc(1em + 8px)
-			overflow hidden
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.1)
-			border-radius 4px
-
-			> .users
-				display block
-				margin 0
-				padding 4px 0
-				max-height 190px
-				max-width 500px
-				overflow auto
-				list-style none
-
-				> li
-					display block
-					padding 4px 12px
-					white-space nowrap
-					overflow hidden
-					font-size 0.9em
-					color rgba(0, 0, 0, 0.8)
-					cursor default
-
-					&, *
-						user-select none
-
-					&:hover
-					&[data-selected='true']
-						color #fff
-						background $theme-color
-
-						.name
-							color #fff
-
-						.username
-							color #fff
-
-					&:active
-						color #fff
-						background darken($theme-color, 10%)
-
-						.name
-							color #fff
-
-						.username
-							color #fff
-
-					.avatar
-						vertical-align middle
-						min-width 28px
-						min-height 28px
-						max-width 28px
-						max-height 28px
-						margin 0 8px 0 0
-						border-radius 100%
-
-					.name
-						margin 0 8px 0 0
-						/*font-weight bold*/
-						font-weight normal
-						color rgba(0, 0, 0, 0.8)
-
-					.username
-						font-weight normal
-						color rgba(0, 0, 0, 0.3)
-
-	</style>
-	<script lang="typescript">
-		import contains from '../../common/scripts/contains';
-
-		this.mixin('api');
-
-		this.q = this.opts.q;
-		this.textarea = this.opts.textarea;
-		this.fetching = true;
-		this.users = [];
-		this.select = -1;
-
-		this.on('mount', () => {
-			this.textarea.addEventListener('keydown', this.onKeydown);
-
-			document.querySelectorAll('body *').forEach(el => {
-				el.addEventListener('mousedown', this.mousedown);
-			});
-
-			this.$root.$data.os.api('users/search_by_username', {
-				query: this.q,
-				limit: 30
-			}).then(users => {
-				this.update({
-					fetching: false,
-					users: users
-				});
-			});
-		});
-
-		this.on('unmount', () => {
-			this.textarea.removeEventListener('keydown', this.onKeydown);
-
-			document.querySelectorAll('body *').forEach(el => {
-				el.removeEventListener('mousedown', this.mousedown);
-			});
-		});
-
-		this.mousedown = e => {
-			if (!contains(this.root, e.target) && (this.root != e.target)) this.close();
-		};
-
-		this.onClick = e => {
-			this.complete(e.item);
-		};
-
-		this.onKeydown = e => {
-			const cancel = () => {
-				e.preventDefault();
-				e.stopPropagation();
-			};
-
-			switch (e.which) {
-				case 10: // [ENTER]
-				case 13: // [ENTER]
-					if (this.select !== -1) {
-						cancel();
-						this.complete(this.users[this.select]);
-					} else {
-						this.close();
-					}
-					break;
-
-				case 27: // [ESC]
-					cancel();
-					this.close();
-					break;
-
-				case 38: // [↑]
-					if (this.select !== -1) {
-						cancel();
-						this.selectPrev();
-					} else {
-						this.close();
-					}
-					break;
-
-				case 9: // [TAB]
-				case 40: // [↓]
-					cancel();
-					this.selectNext();
-					break;
-
-				default:
-					this.close();
-			}
-		};
-
-		this.selectNext = () => {
-			if (++this.select >= this.users.length) this.select = 0;
-			this.applySelect();
-		};
-
-		this.selectPrev = () => {
-			if (--this.select < 0) this.select = this.users.length - 1;
-			this.applySelect();
-		};
-
-		this.applySelect = () => {
-			Array.from(this.$refs.users.children).forEach(el => {
-				el.removeAttribute('data-selected');
-			});
-
-			this.$refs.users.children[this.select].setAttribute('data-selected', 'true');
-			this.$refs.users.children[this.select].focus();
-		};
-
-		this.complete = user => {
-			this.opts.complete(user);
-		};
-
-		this.close = () => {
-			this.opts.close();
-		};
-
-	</script>
-</mk-autocomplete-suggestion>
diff --git a/src/web/app/desktop/views/components/autocomplete.vue b/src/web/app/desktop/views/components/autocomplete.vue
new file mode 100644
index 000000000..a99d405e8
--- /dev/null
+++ b/src/web/app/desktop/views/components/autocomplete.vue
@@ -0,0 +1,190 @@
+<template>
+<div class="mk-autocomplete">
+	<ol class="users" ref="users" v-if="users.length > 0">
+		<li v-for="user in users" @click="complete(user)" @keydown="onKeydown" tabindex="-1">
+			<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=32`" alt=""/>
+			<span class="name">{{ user.name }}</span>
+			<span class="username">@{{ user.username }}</span>
+		</li>
+	</ol>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import contains from '../../../common/scripts/contains';
+
+export default Vue.extend({
+	props: ['q', 'textarea', 'complete', 'close'],
+	data() {
+		return {
+			fetching: true,
+			users: [],
+			select: -1
+		}
+	},
+	mounted() {
+		this.textarea.addEventListener('keydown', this.onKeydown);
+
+		Array.from(document.querySelectorAll('body *')).forEach(el => {
+			el.addEventListener('mousedown', this.onMousedown);
+		});
+
+		(this as any).api('users/search_by_username', {
+			query: this.q,
+			limit: 30
+		}).then(users => {
+			this.users = users;
+			this.fetching = false;
+		});
+	},
+	beforeDestroy() {
+		this.textarea.removeEventListener('keydown', this.onKeydown);
+
+		Array.from(document.querySelectorAll('body *')).forEach(el => {
+			el.removeEventListener('mousedown', this.onMousedown);
+		});
+	},
+	methods: {
+		onMousedown(e) {
+			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
+		},
+
+		onKeydown(e) {
+			const cancel = () => {
+				e.preventDefault();
+				e.stopPropagation();
+			};
+
+			switch (e.which) {
+				case 10: // [ENTER]
+				case 13: // [ENTER]
+					if (this.select !== -1) {
+						cancel();
+						this.complete(this.users[this.select]);
+					} else {
+						this.close();
+					}
+					break;
+
+				case 27: // [ESC]
+					cancel();
+					this.close();
+					break;
+
+				case 38: // [↑]
+					if (this.select !== -1) {
+						cancel();
+						this.selectPrev();
+					} else {
+						this.close();
+					}
+					break;
+
+				case 9: // [TAB]
+				case 40: // [↓]
+					cancel();
+					this.selectNext();
+					break;
+
+				default:
+					this.close();
+			}
+		},
+
+		selectNext() {
+			if (++this.select >= this.users.length) this.select = 0;
+			this.applySelect();
+		},
+
+		selectPrev() {
+			if (--this.select < 0) this.select = this.users.length - 1;
+			this.applySelect();
+		},
+
+		applySelect() {
+			const els = (this.$refs.users as Element).children;
+
+			Array.from(els).forEach(el => {
+				el.removeAttribute('data-selected');
+			});
+
+			els[this.select].setAttribute('data-selected', 'true');
+			(els[this.select] as any).focus();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-autocomplete
+	position absolute
+	z-index 65535
+	margin-top calc(1em + 8px)
+	overflow hidden
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.1)
+	border-radius 4px
+
+	> .users
+		display block
+		margin 0
+		padding 4px 0
+		max-height 190px
+		max-width 500px
+		overflow auto
+		list-style none
+
+		> li
+			display block
+			padding 4px 12px
+			white-space nowrap
+			overflow hidden
+			font-size 0.9em
+			color rgba(0, 0, 0, 0.8)
+			cursor default
+
+			&, *
+				user-select none
+
+			&:hover
+			&[data-selected='true']
+				color #fff
+				background $theme-color
+
+				.name
+					color #fff
+
+				.username
+					color #fff
+
+			&:active
+				color #fff
+				background darken($theme-color, 10%)
+
+				.name
+					color #fff
+
+				.username
+					color #fff
+
+			.avatar
+				vertical-align middle
+				min-width 28px
+				min-height 28px
+				max-width 28px
+				max-height 28px
+				margin 0 8px 0 0
+				border-radius 100%
+
+			.name
+				margin 0 8px 0 0
+				/*font-weight bold*/
+				font-weight normal
+				color rgba(0, 0, 0, 0.8)
+
+			.username
+				font-weight normal
+				color rgba(0, 0, 0, 0.3)
+
+</style>

From 19981f78fc8b7511eb70041355c929e6ec910818 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 20:33:11 +0900
Subject: [PATCH 218/286] wip

---
 src/web/app/{ => common}/filters/bytes.ts | 0
 src/web/app/{ => common}/filters/index.ts | 0
 src/web/app/init.ts                       | 2 +-
 3 files changed, 1 insertion(+), 1 deletion(-)
 rename src/web/app/{ => common}/filters/bytes.ts (100%)
 rename src/web/app/{ => common}/filters/index.ts (100%)

diff --git a/src/web/app/filters/bytes.ts b/src/web/app/common/filters/bytes.ts
similarity index 100%
rename from src/web/app/filters/bytes.ts
rename to src/web/app/common/filters/bytes.ts
diff --git a/src/web/app/filters/index.ts b/src/web/app/common/filters/index.ts
similarity index 100%
rename from src/web/app/filters/index.ts
rename to src/web/app/common/filters/index.ts
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index c3eede0d3..e8ca78927 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -21,7 +21,7 @@ require('./common/views/directives');
 require('./common/views/components');
 
 // Register global filters
-require('./filters');
+require('./common/filters');
 
 Vue.mixin({
 	destroyed(this: any) {

From dfc1c73c9e77916649684c062099ecfec30a8e1e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 20:50:01 +0900
Subject: [PATCH 219/286] wip

---
 src/web/app/auth/tags/form.tag   | 130 ---------------------------
 src/web/app/auth/tags/index.tag  | 143 ------------------------------
 src/web/app/auth/tags/index.ts   |   2 -
 src/web/app/auth/views/form.vue  | 140 +++++++++++++++++++++++++++++
 src/web/app/auth/views/index.vue | 145 +++++++++++++++++++++++++++++++
 5 files changed, 285 insertions(+), 275 deletions(-)
 delete mode 100644 src/web/app/auth/tags/form.tag
 delete mode 100644 src/web/app/auth/tags/index.tag
 delete mode 100644 src/web/app/auth/tags/index.ts
 create mode 100644 src/web/app/auth/views/form.vue
 create mode 100644 src/web/app/auth/views/index.vue

diff --git a/src/web/app/auth/tags/form.tag b/src/web/app/auth/tags/form.tag
deleted file mode 100644
index b1de0baab..000000000
--- a/src/web/app/auth/tags/form.tag
+++ /dev/null
@@ -1,130 +0,0 @@
-<mk-form>
-	<header>
-		<h1><i>{ app.name }</i>があなたの<b>アカウント</b>に<b>アクセス</b>することを<b>許可</b>しますか?</h1><img src={ app.icon_url + '?thumbnail&size=64' }/>
-	</header>
-	<div class="app">
-		<section>
-			<h2>{ app.name }</h2>
-			<p class="nid">{ app.name_id }</p>
-			<p class="description">{ app.description }</p>
-		</section>
-		<section>
-			<h2>このアプリは次の権限を要求しています:</h2>
-			<ul>
-				<template each={ p in app.permission }>
-					<li v-if="p == 'account-read'">アカウントの情報を見る。</li>
-					<li v-if="p == 'account-write'">アカウントの情報を操作する。</li>
-					<li v-if="p == 'post-write'">投稿する。</li>
-					<li v-if="p == 'like-write'">いいねしたりいいね解除する。</li>
-					<li v-if="p == 'following-write'">フォローしたりフォロー解除する。</li>
-					<li v-if="p == 'drive-read'">ドライブを見る。</li>
-					<li v-if="p == 'drive-write'">ドライブを操作する。</li>
-					<li v-if="p == 'notification-read'">通知を見る。</li>
-					<li v-if="p == 'notification-write'">通知を操作する。</li>
-				</template>
-			</ul>
-		</section>
-	</div>
-	<div class="action">
-		<button @click="cancel">キャンセル</button>
-		<button @click="accept">アクセスを許可</button>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> header
-				> h1
-					margin 0
-					padding 32px 32px 20px 32px
-					font-size 24px
-					font-weight normal
-					color #777
-
-					i
-						color #77aeca
-
-						&:before
-							content '「'
-
-						&:after
-							content '」'
-
-					b
-						color #666
-
-				> img
-					display block
-					z-index 1
-					width 84px
-					height 84px
-					margin 0 auto -38px auto
-					border solid 5px #fff
-					border-radius 100%
-					box-shadow 0 2px 2px rgba(0, 0, 0, 0.1)
-
-			> .app
-				padding 44px 16px 0 16px
-				color #555
-				background #eee
-				box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) inset
-
-				&:after
-					content ''
-					display block
-					clear both
-
-				> section
-					float left
-					width 50%
-					padding 8px
-					text-align left
-
-					> h2
-						margin 0
-						font-size 16px
-						color #777
-
-			> .action
-				padding 16px
-
-				> button
-					margin 0 8px
-
-			@media (max-width 600px)
-				> header
-					> img
-						box-shadow none
-
-				> .app
-					box-shadow none
-
-			@media (max-width 500px)
-				> header
-					> h1
-						font-size 16px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.session = this.opts.session;
-		this.app = this.session.app;
-
-		this.cancel = () => {
-			this.$root.$data.os.api('auth/deny', {
-				token: this.session.token
-			}).then(() => {
-				this.$emit('denied');
-			});
-		};
-
-		this.accept = () => {
-			this.$root.$data.os.api('auth/accept', {
-				token: this.session.token
-			}).then(() => {
-				this.$emit('accepted');
-			});
-		};
-	</script>
-</mk-form>
diff --git a/src/web/app/auth/tags/index.tag b/src/web/app/auth/tags/index.tag
deleted file mode 100644
index 56fbbb7da..000000000
--- a/src/web/app/auth/tags/index.tag
+++ /dev/null
@@ -1,143 +0,0 @@
-<mk-index>
-	<main v-if="$root.$data.os.isSignedIn">
-		<p class="fetching" v-if="fetching">読み込み中<mk-ellipsis/></p>
-		<mk-form ref="form" v-if="state == 'waiting'" session={ session }/>
-		<div class="denied" v-if="state == 'denied'">
-			<h1>アプリケーションの連携をキャンセルしました。</h1>
-			<p>このアプリがあなたのアカウントにアクセスすることはありません。</p>
-		</div>
-		<div class="accepted" v-if="state == 'accepted'">
-			<h1>{ session.app.is_authorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました'}</h1>
-			<p v-if="session.app.callback_url">アプリケーションに戻っています<mk-ellipsis/></p>
-			<p v-if="!session.app.callback_url">アプリケーションに戻って、やっていってください。</p>
-		</div>
-		<div class="error" v-if="state == 'fetch-session-error'">
-			<p>セッションが存在しません。</p>
-		</div>
-	</main>
-	<main class="signin" v-if="!$root.$data.os.isSignedIn">
-		<h1>サインインしてください</h1>
-		<mk-signin/>
-	</main>
-	<footer><img src="/assets/auth/logo.svg" alt="Misskey"/></footer>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> main
-				width 100%
-				max-width 500px
-				margin 0 auto
-				text-align center
-				background #fff
-				box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
-
-				> .fetching
-					margin 0
-					padding 32px
-					color #555
-
-				> div
-					padding 64px
-
-					> h1
-						margin 0 0 8px 0
-						padding 0
-						font-size 20px
-						font-weight normal
-
-					> p
-						margin 0
-						color #555
-
-					&.denied > h1
-						color #e65050
-
-					&.accepted > h1
-						color #54af7c
-
-				&.signin
-					padding 32px 32px 16px 32px
-
-					> h1
-						margin 0 0 22px 0
-						padding 0
-						font-size 20px
-						font-weight normal
-						color #555
-
-				@media (max-width 600px)
-					max-width none
-					box-shadow none
-
-				@media (max-width 500px)
-					> div
-						> h1
-							font-size 16px
-
-			> footer
-				> img
-					display block
-					width 64px
-					height 64px
-					margin 0 auto
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-		this.mixin('api');
-
-		this.state = null;
-		this.fetching = true;
-
-		this.token = window.location.href.split('/').pop();
-
-		this.on('mount', () => {
-			if (!this.$root.$data.os.isSignedIn) return;
-
-			// Fetch session
-			this.$root.$data.os.api('auth/session/show', {
-				token: this.token
-			}).then(session => {
-				this.session = session;
-				this.fetching = false;
-
-				// 既に連携していた場合
-				if (this.session.app.is_authorized) {
-					this.$root.$data.os.api('auth/accept', {
-						token: this.session.token
-					}).then(() => {
-						this.accepted();
-					});
-				} else {
-					this.update({
-						state: 'waiting'
-					});
-
-					this.$refs.form.on('denied', () => {
-						this.update({
-							state: 'denied'
-						});
-					});
-
-					this.$refs.form.on('accepted', this.accepted);
-				}
-			}).catch(error => {
-				this.update({
-					fetching: false,
-					state: 'fetch-session-error'
-				});
-			});
-		});
-
-		this.accepted = () => {
-			this.update({
-				state: 'accepted'
-			});
-
-			if (this.session.app.callback_url) {
-				location.href = this.session.app.callback_url + '?token=' + this.session.token;
-			}
-		};
-	</script>
-</mk-index>
diff --git a/src/web/app/auth/tags/index.ts b/src/web/app/auth/tags/index.ts
deleted file mode 100644
index 42dffe67d..000000000
--- a/src/web/app/auth/tags/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-require('./index.tag');
-require('./form.tag');
diff --git a/src/web/app/auth/views/form.vue b/src/web/app/auth/views/form.vue
new file mode 100644
index 000000000..30ad64ed2
--- /dev/null
+++ b/src/web/app/auth/views/form.vue
@@ -0,0 +1,140 @@
+<template>
+<div class="form">
+	<header>
+		<h1><i>{{ app.name }}</i>があなたのアカウントにアクセスすることを<b>許可</b>しますか?</h1>
+		<img :src="`${app.icon_url}?thumbnail&size=64`"/>
+	</header>
+	<div class="app">
+		<section>
+			<h2>{{ app.name }}</h2>
+			<p class="nid">{{ app.name_id }}</p>
+			<p class="description">{{ app.description }}</p>
+		</section>
+		<section>
+			<h2>このアプリは次の権限を要求しています:</h2>
+			<ul>
+				<template v-for="p in app.permission">
+					<li v-if="p == 'account-read'">アカウントの情報を見る。</li>
+					<li v-if="p == 'account-write'">アカウントの情報を操作する。</li>
+					<li v-if="p == 'post-write'">投稿する。</li>
+					<li v-if="p == 'like-write'">いいねしたりいいね解除する。</li>
+					<li v-if="p == 'following-write'">フォローしたりフォロー解除する。</li>
+					<li v-if="p == 'drive-read'">ドライブを見る。</li>
+					<li v-if="p == 'drive-write'">ドライブを操作する。</li>
+					<li v-if="p == 'notification-read'">通知を見る。</li>
+					<li v-if="p == 'notification-write'">通知を操作する。</li>
+				</template>
+			</ul>
+		</section>
+	</div>
+	<div class="action">
+		<button @click="cancel">キャンセル</button>
+		<button @click="accept">アクセスを許可</button>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['session'],
+	computed: {
+		app(): any {
+			return this.session.app;
+		}
+	},
+	methods: {
+		cancel() {
+			(this as any).api('auth/deny', {
+				token: this.session.token
+			}).then(() => {
+				this.$emit('denied');
+			});
+		},
+
+		accept() {
+			(this as any).api('auth/accept', {
+				token: this.session.token
+			}).then(() => {
+				this.$emit('accepted');
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.form
+
+	> header
+		> h1
+			margin 0
+			padding 32px 32px 20px 32px
+			font-size 24px
+			font-weight normal
+			color #777
+
+			i
+				color #77aeca
+
+				&:before
+					content '「'
+
+				&:after
+					content '」'
+
+			b
+				color #666
+
+		> img
+			display block
+			z-index 1
+			width 84px
+			height 84px
+			margin 0 auto -38px auto
+			border solid 5px #fff
+			border-radius 100%
+			box-shadow 0 2px 2px rgba(0, 0, 0, 0.1)
+
+	> .app
+		padding 44px 16px 0 16px
+		color #555
+		background #eee
+		box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) inset
+
+		&:after
+			content ''
+			display block
+			clear both
+
+		> section
+			float left
+			width 50%
+			padding 8px
+			text-align left
+
+			> h2
+				margin 0
+				font-size 16px
+				color #777
+
+	> .action
+		padding 16px
+
+		> button
+			margin 0 8px
+
+	@media (max-width 600px)
+		> header
+			> img
+				box-shadow none
+
+		> .app
+			box-shadow none
+
+	@media (max-width 500px)
+		> header
+			> h1
+				font-size 16px
+
+</style>
diff --git a/src/web/app/auth/views/index.vue b/src/web/app/auth/views/index.vue
new file mode 100644
index 000000000..56a7bac7a
--- /dev/null
+++ b/src/web/app/auth/views/index.vue
@@ -0,0 +1,145 @@
+<template>
+<div class="index">
+	<main v-if="os.isSignedIn">
+		<p class="fetching" v-if="fetching">読み込み中<mk-ellipsis/></p>
+		<fo-rm
+			ref="form"
+			v-if="state == 'waiting'"
+			:session="session"
+			@denied="state = 'denied'"
+			@accepted="accepted"
+		/>
+		<div class="denied" v-if="state == 'denied'">
+			<h1>アプリケーションの連携をキャンセルしました。</h1>
+			<p>このアプリがあなたのアカウントにアクセスすることはありません。</p>
+		</div>
+		<div class="accepted" v-if="state == 'accepted'">
+			<h1>{{ session.app.is_authorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました'}}</h1>
+			<p v-if="session.app.callback_url">アプリケーションに戻っています<mk-ellipsis/></p>
+			<p v-if="!session.app.callback_url">アプリケーションに戻って、やっていってください。</p>
+		</div>
+		<div class="error" v-if="state == 'fetch-session-error'">
+			<p>セッションが存在しません。</p>
+		</div>
+	</main>
+	<main class="signin" v-if="!os.isSignedIn">
+		<h1>サインインしてください</h1>
+		<mk-signin/>
+	</main>
+	<footer><img src="/assets/auth/logo.svg" alt="Misskey"/></footer>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Form from './form.vue';
+
+export default Vue.extend({
+	components: {
+		'fo-rm': Form
+	},
+	data() {
+		return {
+			state: null,
+			session: null,
+			fetching: true,
+			token: window.location.href.split('/').pop()
+		};
+	},
+	mounted() {
+		if (!this.$root.$data.os.isSignedIn) return;
+
+		// Fetch session
+		(this as any).api('auth/session/show', {
+			token: this.token
+		}).then(session => {
+			this.session = session;
+			this.fetching = false;
+
+			// 既に連携していた場合
+			if (this.session.app.is_authorized) {
+				this.$root.$data.os.api('auth/accept', {
+					token: this.session.token
+				}).then(() => {
+					this.accepted();
+				});
+			} else {
+				this.state = 'waiting';
+			}
+		}).catch(error => {
+			this.state = 'fetch-session-error';
+		});
+	},
+	methods: {
+		accepted() {
+			this.state = 'accepted';
+			if (this.session.app.callback_url) {
+				location.href = this.session.app.callback_url + '?token=' + this.session.token;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.index
+
+	> main
+		width 100%
+		max-width 500px
+		margin 0 auto
+		text-align center
+		background #fff
+		box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
+
+		> .fetching
+			margin 0
+			padding 32px
+			color #555
+
+		> div
+			padding 64px
+
+			> h1
+				margin 0 0 8px 0
+				padding 0
+				font-size 20px
+				font-weight normal
+
+			> p
+				margin 0
+				color #555
+
+			&.denied > h1
+				color #e65050
+
+			&.accepted > h1
+				color #54af7c
+
+		&.signin
+			padding 32px 32px 16px 32px
+
+			> h1
+				margin 0 0 22px 0
+				padding 0
+				font-size 20px
+				font-weight normal
+				color #555
+
+		@media (max-width 600px)
+			max-width none
+			box-shadow none
+
+		@media (max-width 500px)
+			> div
+				> h1
+					font-size 16px
+
+	> footer
+		> img
+			display block
+			width 64px
+			height 64px
+			margin 0 auto
+
+</style>

From 3cc45145fe279cd88e2b7542ac96af0cbcb11142 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 22:31:12 +0900
Subject: [PATCH 220/286] wip

---
 .../views/pages/user/user-followers-you-know.vue     | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/web/app/desktop/views/pages/user/user-followers-you-know.vue b/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
index c58eb75bc..181d5824d 100644
--- a/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
+++ b/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="mk-user-followers-you-know">
 	<p class="title">%fa:users%%i18n:desktop.tags.mk-user.followers-you-know.title%</p>
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p>
-	<div v-if="!initializing && users.length > 0">
-	<template each={ user in users }>
-		<a href={ '/' + user.username }><img src={ user.avatar_url + '?thumbnail&size=64' } alt={ user.name }/></a>
-	</template>
+	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p>
+	<div v-if="!fetching && users.length > 0">
+	<router-link v-for="user in users" to="`/${user.username}`" :key="user.id">
+		<img :src="`${user.avatar_url}?thumbnail&size=64`" :alt="user.name" v-user-preview="user.id"/>
+	</router-link>
 	</div>
-	<p class="empty" v-if="!initializing && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
+	<p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
 </div>
 </template>
 

From e1259409e914baaa399e2866feb887b3beddbdf7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 22:53:34 +0900
Subject: [PATCH 221/286] wip

---
 src/web/app/config.ts                         |  2 +
 src/web/app/desktop/api/post.ts               |  6 +++
 src/web/app/desktop/script.ts                 |  4 +-
 src/web/app/desktop/views/components/index.ts | 14 -------
 ...ader-account.vue => ui.header.account.vue} |  4 +-
 ...i-header-clock.vue => ui.header.clock.vue} |  4 +-
 .../{ui-header-nav.vue => ui.header.nav.vue}  | 10 +++--
 ...ations.vue => ui.header.notifications.vue} |  8 ++--
 ...der-post-button.vue => ui.header.post.vue} |  4 +-
 ...header-search.vue => ui.header.search.vue} |  4 +-
 .../{ui-header.vue => ui.header.vue}          | 39 ++++++++++++++-----
 src/web/app/desktop/views/components/ui.vue   | 14 +++----
 .../pages/user/user-followers-you-know.vue    |  2 +-
 .../desktop/views/pages/user/user-friends.vue |  4 +-
 src/web/app/desktop/views/pages/user/user.vue | 29 +++++++++-----
 src/web/app/init.ts                           |  2 +
 16 files changed, 89 insertions(+), 61 deletions(-)
 create mode 100644 src/web/app/desktop/api/post.ts
 rename src/web/app/desktop/views/components/{ui-header-account.vue => ui.header.account.vue} (98%)
 rename src/web/app/desktop/views/components/{ui-header-clock.vue => ui.header.clock.vue} (96%)
 rename src/web/app/desktop/views/components/{ui-header-nav.vue => ui.header.nav.vue} (95%)
 rename src/web/app/desktop/views/components/{ui-header-notifications.vue => ui.header.notifications.vue} (96%)
 rename src/web/app/desktop/views/components/{ui-header-post-button.vue => ui.header.post.vue} (93%)
 rename src/web/app/desktop/views/components/{ui-header-search.vue => ui.header.search.vue} (92%)
 rename src/web/app/desktop/views/components/{ui-header.vue => ui.header.vue} (63%)

diff --git a/src/web/app/config.ts b/src/web/app/config.ts
index 25381ecce..2461b2215 100644
--- a/src/web/app/config.ts
+++ b/src/web/app/config.ts
@@ -5,6 +5,7 @@ declare const _DOCS_URL_: string;
 declare const _STATS_URL_: string;
 declare const _STATUS_URL_: string;
 declare const _DEV_URL_: string;
+declare const _CH_URL_: string;
 declare const _LANG_: string;
 declare const _RECAPTCHA_SITEKEY_: string;
 declare const _SW_PUBLICKEY_: string;
@@ -19,6 +20,7 @@ export const docsUrl = _DOCS_URL_;
 export const statsUrl = _STATS_URL_;
 export const statusUrl = _STATUS_URL_;
 export const devUrl = _DEV_URL_;
+export const chUrl = _CH_URL_;
 export const lang = _LANG_;
 export const recaptchaSitekey = _RECAPTCHA_SITEKEY_;
 export const swPublickey = _SW_PUBLICKEY_;
diff --git a/src/web/app/desktop/api/post.ts b/src/web/app/desktop/api/post.ts
new file mode 100644
index 000000000..4eebd747f
--- /dev/null
+++ b/src/web/app/desktop/api/post.ts
@@ -0,0 +1,6 @@
+import PostFormWindow from '../views/components/post-form-window.vue';
+
+export default function() {
+	const vm = new PostFormWindow().$mount();
+	document.body.appendChild(vm.$el);
+}
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index 7278c9af1..251a2a161 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -14,6 +14,7 @@ import chooseDriveFolder from './api/choose-drive-folder';
 import chooseDriveFile from './api/choose-drive-file';
 import dialog from './api/dialog';
 import input from './api/input';
+import post from './api/post';
 
 import MkIndex from './views/pages/index.vue';
 import MkUser from './views/pages/user/user.vue';
@@ -37,7 +38,8 @@ init(async (launch) => {
 		chooseDriveFolder,
 		chooseDriveFile,
 		dialog,
-		input
+		input,
+		post
 	});
 
 	/**
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 8e48d67b9..fb8ded9c0 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -1,13 +1,6 @@
 import Vue from 'vue';
 
 import ui from './ui.vue';
-import uiHeader from './ui-header.vue';
-import uiHeaderAccount from './ui-header-account.vue';
-import uiHeaderClock from './ui-header-clock.vue';
-import uiHeaderNav from './ui-header-nav.vue';
-import uiHeaderNotifications from './ui-header-notifications.vue';
-import uiHeaderPostButton from './ui-header-post-button.vue';
-import uiHeaderSearch from './ui-header-search.vue';
 import uiNotification from './ui-notification.vue';
 import home from './home.vue';
 import timeline from './timeline.vue';
@@ -46,13 +39,6 @@ import wBroadcast from './widgets/broadcast.vue';
 import wTimemachine from './widgets/timemachine.vue';
 
 Vue.component('mk-ui', ui);
-Vue.component('mk-ui-header', uiHeader);
-Vue.component('mk-ui-header-account', uiHeaderAccount);
-Vue.component('mk-ui-header-clock', uiHeaderClock);
-Vue.component('mk-ui-header-nav', uiHeaderNav);
-Vue.component('mk-ui-header-notifications', uiHeaderNotifications);
-Vue.component('mk-ui-header-post-button', uiHeaderPostButton);
-Vue.component('mk-ui-header-search', uiHeaderSearch);
 Vue.component('mk-ui-notification', uiNotification);
 Vue.component('mk-home', home);
 Vue.component('mk-timeline', timeline);
diff --git a/src/web/app/desktop/views/components/ui-header-account.vue b/src/web/app/desktop/views/components/ui.header.account.vue
similarity index 98%
rename from src/web/app/desktop/views/components/ui-header-account.vue
rename to src/web/app/desktop/views/components/ui.header.account.vue
index 337c47674..3728f94be 100644
--- a/src/web/app/desktop/views/components/ui-header-account.vue
+++ b/src/web/app/desktop/views/components/ui.header.account.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-ui-header-account">
+<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.avatar_url }?thumbnail&size=64`" alt="avatar"/>
@@ -81,7 +81,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-ui-header-account
+.account
 	> .header
 		display block
 		margin 0
diff --git a/src/web/app/desktop/views/components/ui-header-clock.vue b/src/web/app/desktop/views/components/ui.header.clock.vue
similarity index 96%
rename from src/web/app/desktop/views/components/ui-header-clock.vue
rename to src/web/app/desktop/views/components/ui.header.clock.vue
index cfed1e84a..cd23a6750 100644
--- a/src/web/app/desktop/views/components/ui-header-clock.vue
+++ b/src/web/app/desktop/views/components/ui.header.clock.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-ui-header-clock">
+<div class="clock">
 	<div class="header">
 		<time ref="time">
 			<span class="yyyymmdd">{{ yyyy }}/{{ mm }}/{{ dd }}</span>
@@ -56,7 +56,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-ui-header-clock
+.clock
 	display inline-block
 	overflow visible
 
diff --git a/src/web/app/desktop/views/components/ui-header-nav.vue b/src/web/app/desktop/views/components/ui.header.nav.vue
similarity index 95%
rename from src/web/app/desktop/views/components/ui-header-nav.vue
rename to src/web/app/desktop/views/components/ui.header.nav.vue
index cf276dc5c..5895255ff 100644
--- a/src/web/app/desktop/views/components/ui-header-nav.vue
+++ b/src/web/app/desktop/views/components/ui.header.nav.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-ui-header-nav">
+<div class="nav">
 	<ul>
 		<template v-if="os.isSignedIn">
 			<li class="home" :class="{ active: page == 'home' }">
@@ -17,7 +17,7 @@
 			</li>
 		</template>
 		<li class="ch">
-			<a :href="_CH_URL_" target="_blank">
+			<a :href="chUrl" target="_blank">
 				%fa:tv%
 				<p>%i18n:desktop.tags.mk-ui-header-nav.ch%</p>
 			</a>
@@ -34,6 +34,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import { chUrl } from '../../../config';
 import MkMessagingWindow from './messaging-window.vue';
 
 export default Vue.extend({
@@ -41,7 +42,8 @@ export default Vue.extend({
 		return {
 			hasUnreadMessagingMessages: false,
 			connection: null,
-			connectionId: null
+			connectionId: null,
+			chUrl
 		};
 	},
 	mounted() {
@@ -84,7 +86,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-ui-header-nav
+.nav
 	display inline-block
 	margin 0
 	padding 0
diff --git a/src/web/app/desktop/views/components/ui-header-notifications.vue b/src/web/app/desktop/views/components/ui.header.notifications.vue
similarity index 96%
rename from src/web/app/desktop/views/components/ui-header-notifications.vue
rename to src/web/app/desktop/views/components/ui.header.notifications.vue
index d4dc553c5..5467dda85 100644
--- a/src/web/app/desktop/views/components/ui-header-notifications.vue
+++ b/src/web/app/desktop/views/components/ui.header.notifications.vue
@@ -1,9 +1,9 @@
 <template>
-<div class="mk-ui-header-notifications">
+<div class="notifications">
 	<button :data-active="isOpen" @click="toggle" title="%i18n:desktop.tags.mk-ui-header-notifications.title%">
 		%fa:R bell%<template v-if="hasUnreadNotifications">%fa:circle%</template>
 	</button>
-	<div class="notifications" v-if="isOpen">
+	<div class="pop" v-if="isOpen">
 		<mk-notifications/>
 	</div>
 </div>
@@ -82,7 +82,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-ui-header-notifications
+.notifications
 
 	> button
 		display block
@@ -114,7 +114,7 @@ export default Vue.extend({
 			font-size 10px
 			color $theme-color
 
-	> .notifications
+	> .pop
 		display block
 		position absolute
 		top 56px
diff --git a/src/web/app/desktop/views/components/ui-header-post-button.vue b/src/web/app/desktop/views/components/ui.header.post.vue
similarity index 93%
rename from src/web/app/desktop/views/components/ui-header-post-button.vue
rename to src/web/app/desktop/views/components/ui.header.post.vue
index 754e05b23..10bce0622 100644
--- a/src/web/app/desktop/views/components/ui-header-post-button.vue
+++ b/src/web/app/desktop/views/components/ui.header.post.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-ui-header-post-button">
+<div class="post">
 	<button @click="post" title="%i18n:desktop.tags.mk-ui-header-post-button.post%">%fa:pencil-alt%</button>
 </div>
 </template>
@@ -17,7 +17,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-ui-header-post-button
+.post
 	display inline-block
 	padding 8px
 	height 100%
diff --git a/src/web/app/desktop/views/components/ui-header-search.vue b/src/web/app/desktop/views/components/ui.header.search.vue
similarity index 92%
rename from src/web/app/desktop/views/components/ui-header-search.vue
rename to src/web/app/desktop/views/components/ui.header.search.vue
index 84ca9848c..c063de6bb 100644
--- a/src/web/app/desktop/views/components/ui-header-search.vue
+++ b/src/web/app/desktop/views/components/ui.header.search.vue
@@ -1,5 +1,5 @@
 <template>
-<form class="mk-ui-header-search" @submit.prevent="onSubmit">
+<form class="search" @submit.prevent="onSubmit">
 	%fa:search%
 	<input v-model="q" type="search" placeholder="%i18n:desktop.tags.mk-ui-header-search.placeholder%"/>
 	<div class="result"></div>
@@ -24,7 +24,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-ui-header-search
+.search
 
 	> [data-fa]
 		display block
diff --git a/src/web/app/desktop/views/components/ui-header.vue b/src/web/app/desktop/views/components/ui.header.vue
similarity index 63%
rename from src/web/app/desktop/views/components/ui-header.vue
rename to src/web/app/desktop/views/components/ui.header.vue
index 6b89985ad..ef5e3a95d 100644
--- a/src/web/app/desktop/views/components/ui-header.vue
+++ b/src/web/app/desktop/views/components/ui.header.vue
@@ -1,19 +1,19 @@
 <template>
-<div class="mk-ui-header">
+<div class="header">
 	<mk-special-message/>
 	<div class="main">
 		<div class="backdrop"></div>
 		<div class="main">
 			<div class="container">
 				<div class="left">
-					<mk-ui-header-nav/>
+					<x-nav/>
 				</div>
 				<div class="right">
-					<mk-ui-header-search/>
-					<mk-ui-header-account v-if="os.isSignedIn"/>
-					<mk-ui-header-notifications v-if="os.isSignedIn"/>
-					<mk-ui-header-post-button v-if="os.isSignedIn"/>
-					<mk-ui-header-clock/>
+					<x-search/>
+					<x-account v-if="os.isSignedIn"/>
+					<x-notifications v-if="os.isSignedIn"/>
+					<x-post v-if="os.isSignedIn"/>
+					<x-clock/>
 				</div>
 			</div>
 		</div>
@@ -21,9 +21,30 @@
 </div>
 </template>
 
+<script lang="ts">
+import Vue from 'vue';
+
+import XNav from './ui.header.nav.vue';
+import XSearch from './ui.header.search.vue';
+import XAccount from './ui.header.account.vue';
+import XNotifications from './ui.header.notifications.vue';
+import XPost from './ui.header.post.vue';
+import XClock from './ui.header.clock.vue';
+
+export default Vue.extend({
+	components: {
+		'x-nav': XNav,
+		'x-search': XSearch,
+		'x-account': XAccount,
+		'x-notifications': XNotifications,
+		'x-post': XPost,
+		'x-clock': XClock,
+	}
+});
+</script>
+
 <style lang="stylus" scoped>
-.mk-ui-header
-	display block
+.header
 	position -webkit-sticky
 	position sticky
 	top 0
diff --git a/src/web/app/desktop/views/components/ui.vue b/src/web/app/desktop/views/components/ui.vue
index af39dff7a..9cd12f964 100644
--- a/src/web/app/desktop/views/components/ui.vue
+++ b/src/web/app/desktop/views/components/ui.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	<mk-ui-header/>
+	<x-header/>
 	<div class="content">
 		<slot></slot>
 	</div>
@@ -10,9 +10,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import MkPostFormWindow from './post-form-window.vue';
+import XHeader from './ui.header.vue';
 
 export default Vue.extend({
+	components: {
+		'x-header': XHeader
+	},
 	mounted() {
 		document.addEventListener('keydown', this.onKeydown);
 	},
@@ -20,17 +23,12 @@ export default Vue.extend({
 		document.removeEventListener('keydown', this.onKeydown);
 	},
 	methods: {
-		openPostForm() {
-			document.body.appendChild(new MkPostFormWindow({
-				parent: this
-			}).$mount().$el);
-		},
 		onKeydown(e) {
 			if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return;
 
 			if (e.which == 80 || e.which == 78) { // p or n
 				e.preventDefault();
-				this.openPostForm();
+				(this as any).apis.post();
 			}
 		}
 	}
diff --git a/src/web/app/desktop/views/pages/user/user-followers-you-know.vue b/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
index 181d5824d..6f6673a7a 100644
--- a/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
+++ b/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
@@ -3,7 +3,7 @@
 	<p class="title">%fa:users%%i18n:desktop.tags.mk-user.followers-you-know.title%</p>
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p>
 	<div v-if="!fetching && users.length > 0">
-	<router-link v-for="user in users" to="`/${user.username}`" :key="user.id">
+	<router-link v-for="user in users" :to="`/${user.username}`" :key="user.id">
 		<img :src="`${user.avatar_url}?thumbnail&size=64`" :alt="user.name" v-user-preview="user.id"/>
 	</router-link>
 	</div>
diff --git a/src/web/app/desktop/views/pages/user/user-friends.vue b/src/web/app/desktop/views/pages/user/user-friends.vue
index a144ca2ad..b173e4296 100644
--- a/src/web/app/desktop/views/pages/user/user-friends.vue
+++ b/src/web/app/desktop/views/pages/user/user-friends.vue
@@ -4,11 +4,11 @@
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.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.username}`">
+			<router-link class="avatar-anchor" :to="`/${friend.username}`">
 				<img class="avatar" :src="`${friend.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/>
 			</router-link>
 			<div class="body">
-				<router-link class="name" to="`/${friend.username}`" v-user-preview="friend.id">{{ friend.name }}</router-link>
+				<router-link class="name" :to="`/${friend.username}`" v-user-preview="friend.id">{{ friend.name }}</router-link>
 				<p class="username">@{{ friend.username }}</p>
 			</div>
 			<mk-follow-button :user="friend"/>
diff --git a/src/web/app/desktop/views/pages/user/user.vue b/src/web/app/desktop/views/pages/user/user.vue
index def9ced36..84f31e854 100644
--- a/src/web/app/desktop/views/pages/user/user.vue
+++ b/src/web/app/desktop/views/pages/user/user.vue
@@ -30,16 +30,25 @@ export default Vue.extend({
 			user: null
 		};
 	},
-	mounted() {
-		Progress.start();
-		(this as any).api('users/show', {
-			username: this.$route.params.user
-		}).then(user => {
-			this.user = user;
-			this.fetching = false;
-			Progress.done();
-			document.title = user.name + ' | Misskey';
-		});
+	created() {
+		this.fetch();
+	},
+	watch: {
+		$route: 'fetch'
+	},
+	methods: {
+		fetch() {
+			this.fetching = true;
+			Progress.start();
+			(this as any).api('users/show', {
+				username: this.$route.params.user
+			}).then(user => {
+				this.user = user;
+				this.fetching = false;
+				Progress.done();
+				document.title = user.name + ' | Misskey';
+			});
+		}
 	}
 });
 </script>
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index e8ca78927..9e49c4f0f 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -105,6 +105,8 @@ type API = {
 		placeholder?: string;
 		default?: string;
 	}) => Promise<string>;
+
+	post: () => void;
 };
 
 // MiOSを初期化してコールバックする

From 78160188d9c1529a04a383f10095c8b82302aa34 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 23:02:24 +0900
Subject: [PATCH 222/286] wip

---
 src/web/app/desktop/views/components/index.ts |  4 --
 ...ost-detail-sub.vue => post-detail.sub.vue} |  4 +-
 .../desktop/views/components/post-detail.vue  | 45 +++++++++++++++++--
 ...{posts-post-sub.vue => posts.post.sub.vue} |  4 +-
 .../{posts-post.vue => posts.post.vue}        | 14 +++---
 .../app/desktop/views/components/posts.vue    |  6 ++-
 6 files changed, 58 insertions(+), 19 deletions(-)
 rename src/web/app/desktop/views/components/{post-detail-sub.vue => post-detail.sub.vue} (96%)
 rename src/web/app/desktop/views/components/{posts-post-sub.vue => posts.post.sub.vue} (96%)
 rename src/web/app/desktop/views/components/{posts-post.vue => posts.post.vue} (98%)

diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index fb8ded9c0..cbe145daf 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -5,8 +5,6 @@ import uiNotification from './ui-notification.vue';
 import home from './home.vue';
 import timeline from './timeline.vue';
 import posts from './posts.vue';
-import postsPost from './posts-post.vue';
-import postsPostSub from './posts-post-sub.vue';
 import subPostContent from './sub-post-content.vue';
 import window from './window.vue';
 import postFormWindow from './post-form-window.vue';
@@ -43,8 +41,6 @@ Vue.component('mk-ui-notification', uiNotification);
 Vue.component('mk-home', home);
 Vue.component('mk-timeline', timeline);
 Vue.component('mk-posts', posts);
-Vue.component('mk-posts-post', postsPost);
-Vue.component('mk-posts-post-sub', postsPostSub);
 Vue.component('mk-sub-post-content', subPostContent);
 Vue.component('mk-window', window);
 Vue.component('mk-post-form-window', postFormWindow);
diff --git a/src/web/app/desktop/views/components/post-detail-sub.vue b/src/web/app/desktop/views/components/post-detail.sub.vue
similarity index 96%
rename from src/web/app/desktop/views/components/post-detail-sub.vue
rename to src/web/app/desktop/views/components/post-detail.sub.vue
index 320720dfb..69ced0925 100644
--- a/src/web/app/desktop/views/components/post-detail-sub.vue
+++ b/src/web/app/desktop/views/components/post-detail.sub.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-post-detail-sub" :title="title">
+<div class="sub" :title="title">
 	<a class="avatar-anchor" href={ '/' + post.user.username }>
 		<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar" v-user-preview={ post.user_id }/>
 	</a>
@@ -40,7 +40,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-post-detail-sub
+.sub
 	margin 0
 	padding 20px 32px
 	background #fdfdfd
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index 429b3549b..c9fe00fca 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -11,10 +11,10 @@
 		<template v-if="contextFetching">%fa:spinner .pulse%</template>
 	</button>
 	<div class="context">
-		<mk-post-detail-sub v-for="post in context" :key="post.id" :post="post"/>
+		<x-sub v-for="post in context" :key="post.id" :post="post"/>
 	</div>
 	<div class="reply-to" v-if="p.reply">
-		<mk-post-detail-sub :post="p.reply"/>
+		<x-sub :post="p.reply"/>
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
@@ -62,7 +62,7 @@
 		</footer>
 	</article>
 	<div class="replies" v-if="!compact">
-		<mk-post-detail-sub v-for="post in nreplies" :key="post.id" :post="post"/>
+		<x-sub v-for="post in replies" :key="post.id" :post="post"/>
 	</div>
 </div>
 </template>
@@ -71,7 +71,16 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 
+import MkPostFormWindow from './post-form-window.vue';
+import MkRepostFormWindow from './repost-form-window.vue';
+import MkPostMenu from '../../../common/views/components/post-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './post-detail.sub.vue';
+
 export default Vue.extend({
+	components: {
+		'x-sub': XSub
+	},
 	props: {
 		post: {
 			type: Object,
@@ -137,6 +146,36 @@ export default Vue.extend({
 				this.contextFetching = false;
 				this.context = context.reverse();
 			});
+		},
+		reply() {
+			document.body.appendChild(new MkPostFormWindow({
+				propsData: {
+					reply: this.p
+				}
+			}).$mount().$el);
+		},
+		repost() {
+			document.body.appendChild(new MkRepostFormWindow({
+				propsData: {
+					post: this.p
+				}
+			}).$mount().$el);
+		},
+		react() {
+			document.body.appendChild(new MkReactionPicker({
+				propsData: {
+					source: this.$refs.reactButton,
+					post: this.p
+				}
+			}).$mount().$el);
+		},
+		menu() {
+			document.body.appendChild(new MkPostMenu({
+				propsData: {
+					source: this.$refs.menuButton,
+					post: this.p
+				}
+			}).$mount().$el);
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/posts-post-sub.vue b/src/web/app/desktop/views/components/posts.post.sub.vue
similarity index 96%
rename from src/web/app/desktop/views/components/posts-post-sub.vue
rename to src/web/app/desktop/views/components/posts.post.sub.vue
index cccc24653..dffa8f5a4 100644
--- a/src/web/app/desktop/views/components/posts-post-sub.vue
+++ b/src/web/app/desktop/views/components/posts.post.sub.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-posts-post-sub" :title="title">
+<div class="sub" :title="title">
 	<a class="avatar-anchor" :href="`/${post.user.username}`">
 		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar" v-user-preview="post.user_id"/>
 	</a>
@@ -33,7 +33,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-posts-post-sub
+.sub
 	margin 0
 	padding 0
 	font-size 0.9em
diff --git a/src/web/app/desktop/views/components/posts-post.vue b/src/web/app/desktop/views/components/posts.post.vue
similarity index 98%
rename from src/web/app/desktop/views/components/posts-post.vue
rename to src/web/app/desktop/views/components/posts.post.vue
index f16811609..993bba58c 100644
--- a/src/web/app/desktop/views/components/posts-post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -1,7 +1,7 @@
 <template>
-<div class="mk-posts-post" tabindex="-1" :title="title" @keydown="onKeydown">
+<div class="post" tabindex="-1" :title="title" @keydown="onKeydown">
 	<div class="reply-to" v-if="p.reply">
-		<mk-posts-post-sub post="p.reply"/>
+		<x-sub post="p.reply"/>
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
@@ -80,6 +80,7 @@ import MkPostFormWindow from './post-form-window.vue';
 import MkRepostFormWindow from './repost-form-window.vue';
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './posts.post.sub.vue';
 
 function focus(el, fn) {
 	const target = fn(el);
@@ -93,6 +94,9 @@ function focus(el, fn) {
 }
 
 export default Vue.extend({
+	components: {
+		'x-sub': XSub
+	},
 	props: ['post'],
 	data() {
 		return {
@@ -180,7 +184,6 @@ export default Vue.extend({
 		},
 		reply() {
 			document.body.appendChild(new MkPostFormWindow({
-
 				propsData: {
 					reply: this.p
 				}
@@ -188,7 +191,6 @@ export default Vue.extend({
 		},
 		repost() {
 			document.body.appendChild(new MkRepostFormWindow({
-
 				propsData: {
 					post: this.p
 				}
@@ -196,7 +198,6 @@ export default Vue.extend({
 		},
 		react() {
 			document.body.appendChild(new MkReactionPicker({
-
 				propsData: {
 					source: this.$refs.reactButton,
 					post: this.p
@@ -205,7 +206,6 @@ export default Vue.extend({
 		},
 		menu() {
 			document.body.appendChild(new MkPostMenu({
-
 				propsData: {
 					source: this.$refs.menuButton,
 					post: this.p
@@ -253,7 +253,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-posts-post
+.post
 	margin 0
 	padding 0
 	background #fff
diff --git a/src/web/app/desktop/views/components/posts.vue b/src/web/app/desktop/views/components/posts.vue
index 6c73731bf..bda24e143 100644
--- a/src/web/app/desktop/views/components/posts.vue
+++ b/src/web/app/desktop/views/components/posts.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-posts">
 	<template v-for="(post, i) in _posts">
-		<mk-posts-post :post.sync="post" :key="post.id"/>
+		<x-post :post.sync="post" :key="post.id"/>
 		<p class="date" :key="post.id + '-time'" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date"><span>%fa:angle-up%{{ post._datetext }}</span><span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span></p>
 	</template>
 	<footer>
@@ -12,8 +12,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import XPost from './posts.post.vue';
 
 export default Vue.extend({
+	components: {
+		'x-post': XPost
+	},
 	props: {
 		posts: {
 			type: Array,

From dea85c3a976497a7d54eecc7b3386452e8cd4f0c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 23:12:29 +0900
Subject: [PATCH 223/286] wip

---
 src/web/app/desktop/views/components/post-preview.vue   | 5 +----
 src/web/app/desktop/views/components/posts.post.sub.vue | 4 +---
 src/web/app/desktop/views/components/posts.post.vue     | 5 +----
 src/web/app/desktop/views/components/ui.header.post.vue | 2 +-
 4 files changed, 4 insertions(+), 12 deletions(-)

diff --git a/src/web/app/desktop/views/components/post-preview.vue b/src/web/app/desktop/views/components/post-preview.vue
index 7452bffe2..b39ad3db4 100644
--- a/src/web/app/desktop/views/components/post-preview.vue
+++ b/src/web/app/desktop/views/components/post-preview.vue
@@ -72,9 +72,7 @@ export default Vue.extend({
 				padding 0
 				color #607073
 				font-size 1em
-				line-height 1.1em
-				font-weight 700
-				text-align left
+				font-weight bold
 				text-decoration none
 				white-space normal
 
@@ -82,7 +80,6 @@ export default Vue.extend({
 					text-decoration underline
 
 			> .username
-				text-align left
 				margin 0 .5em 0 0
 				color #d1d8da
 
diff --git a/src/web/app/desktop/views/components/posts.post.sub.vue b/src/web/app/desktop/views/components/posts.post.sub.vue
index dffa8f5a4..4e52d1d70 100644
--- a/src/web/app/desktop/views/components/posts.post.sub.vue
+++ b/src/web/app/desktop/views/components/posts.post.sub.vue
@@ -80,8 +80,7 @@ export default Vue.extend({
 					overflow hidden
 					color #607073
 					font-size 1em
-					font-weight 700
-					text-align left
+					font-weight bold
 					text-decoration none
 					text-overflow ellipsis
 
@@ -89,7 +88,6 @@ export default Vue.extend({
 						text-decoration underline
 
 				> .username
-					text-align left
 					margin 0 .5em 0 0
 					color #d1d8da
 
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 993bba58c..05934571a 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -371,8 +371,7 @@ export default Vue.extend({
 					overflow hidden
 					color #777
 					font-size 1em
-					font-weight 700
-					text-align left
+					font-weight bold
 					text-decoration none
 					text-overflow ellipsis
 
@@ -380,7 +379,6 @@ export default Vue.extend({
 						text-decoration underline
 
 				> .is-bot
-					text-align left
 					margin 0 .5em 0 0
 					padding 1px 6px
 					font-size 12px
@@ -389,7 +387,6 @@ export default Vue.extend({
 					border-radius 3px
 
 				> .username
-					text-align left
 					margin 0 .5em 0 0
 					color #ccc
 
diff --git a/src/web/app/desktop/views/components/ui.header.post.vue b/src/web/app/desktop/views/components/ui.header.post.vue
index 10bce0622..e8ed380f0 100644
--- a/src/web/app/desktop/views/components/ui.header.post.vue
+++ b/src/web/app/desktop/views/components/ui.header.post.vue
@@ -10,7 +10,7 @@ import Vue from 'vue';
 export default Vue.extend({
 	methods: {
 		post() {
-			(this.$parent.$parent as any).openPostForm();
+			(this as any).apis.post();
 		}
 	}
 });

From 648e691b8cf44c41be7e56d65e8deccd7b76271e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 23:17:04 +0900
Subject: [PATCH 224/286] wip

---
 src/web/app/common/views/components/index.ts     | 2 ++
 src/web/app/common/views/components/post-html.ts | 2 +-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 646fa3b71..021e45a8d 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -18,6 +18,7 @@ import messaging from './messaging.vue';
 import messagingForm from './messaging-form.vue';
 import messagingRoom from './messaging-room.vue';
 import messagingMessage from './messaging-message.vue';
+import urlPreview from './url-preview.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
@@ -37,3 +38,4 @@ Vue.component('mk-messaging', messaging);
 Vue.component('mk-messaging-form', messagingForm);
 Vue.component('mk-messaging-room', messagingRoom);
 Vue.component('mk-messaging-message', messagingMessage);
+Vue.component('mk-url-preview', urlPreview);
diff --git a/src/web/app/common/views/components/post-html.ts b/src/web/app/common/views/components/post-html.ts
index 88ced0342..d365bdc49 100644
--- a/src/web/app/common/views/components/post-html.ts
+++ b/src/web/app/common/views/components/post-html.ts
@@ -44,7 +44,7 @@ export default Vue.component('mk-post-html', {
 				case 'url':
 					return createElement(MkUrl, {
 						props: {
-							href: escape(token.content),
+							url: escape(token.content),
 							target: '_blank'
 						}
 					});

From 7779e04fc59f44ab139896c4d5efc31d93f9905b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 23:22:19 +0900
Subject: [PATCH 225/286] wip

---
 ...rofile-setting.vue => settings.profile.vue} | 18 ++++++++++++------
 .../app/desktop/views/components/settings.vue  |  6 +++---
 2 files changed, 15 insertions(+), 9 deletions(-)
 rename src/web/app/desktop/views/components/{profile-setting.vue => settings.profile.vue} (84%)

diff --git a/src/web/app/desktop/views/components/profile-setting.vue b/src/web/app/desktop/views/components/settings.profile.vue
similarity index 84%
rename from src/web/app/desktop/views/components/profile-setting.vue
rename to src/web/app/desktop/views/components/settings.profile.vue
index b61de33ef..c8834ca25 100644
--- a/src/web/app/desktop/views/components/profile-setting.vue
+++ b/src/web/app/desktop/views/components/settings.profile.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-profile-setting">
+<div class="profile">
 	<label class="avatar ui from group">
 		<p>%i18n:desktop.tags.mk-profile-setting.avatar%</p>
 		<img class="avatar" :src="`${os.i.avatar_url}?thumbnail&size=64`" alt="avatar"/>
@@ -32,12 +32,18 @@ import notify from '../../scripts/notify';
 export default Vue.extend({
 	data() {
 		return {
-			name: (this as any).os.i.name,
-			location: (this as any).os.i.location,
-			description: (this as any).os.i.description,
-			birthday: (this as any).os.i.birthday,
+			name: null,
+			location: null,
+			description: null,
+			birthday: null,
 		};
 	},
+	created() {
+		this.name = (this as any).os.i.name;
+		this.location = (this as any).os.i.profile.location;
+		this.description = (this as any).os.i.description;
+		this.birthday = (this as any).os.i.profile.birthday;
+	},
 	methods: {
 		updateAvatar() {
 			(this as any).apis.chooseDriveFile({
@@ -61,7 +67,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-profile-setting
+.profile
 	> .avatar
 		> img
 			display inline-block
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index e9a9bbfa8..681e373ed 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -15,7 +15,7 @@
 	<div class="pages">
 		<section class="profile" v-show="page == 'profile'">
 			<h1>%i18n:desktop.tags.mk-settings.profile%</h1>
-			<mk-profile-setting/>
+			<x-profile/>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -73,11 +73,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import MkProfileSetting from './profile-setting.vue';
+import XProfile from './settings.profile.vue';
 
 export default Vue.extend({
 	components: {
-		'mk-profie-setting': MkProfileSetting
+		'x-profile': XProfile
 	},
 	data() {
 		return {

From e87a5feffa4c5c9e168445eb194240471bbd17cc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 23:26:41 +0900
Subject: [PATCH 226/286] wip

---
 src/web/app/desktop/views/components/drive-window.vue | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/web/app/desktop/views/components/drive-window.vue b/src/web/app/desktop/views/components/drive-window.vue
index 5a6b7c1b5..9fd5df830 100644
--- a/src/web/app/desktop/views/components/drive-window.vue
+++ b/src/web/app/desktop/views/components/drive-window.vue
@@ -4,7 +4,7 @@
 		<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:desktop.tags.mk-drive-browser-window.used%</p>
 		<span :class="$style.title">%fa:cloud%%i18n:desktop.tags.mk-drive-browser-window.drive%</span>
 	</template>
-	<mk-drive multiple :init-folder="folder" ref="browser"/>
+	<mk-drive :class="$style.browser" multiple :init-folder="folder" ref="browser"/>
 </mk-window>
 </template>
 
@@ -49,5 +49,8 @@ export default Vue.extend({
 	margin 0
 	font-size 80%
 
+.browser
+	height 100%
+
 </style>
 

From 204d4dfe31cb2c5e25f25a6c1bf37776eb6cd6e8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 20 Feb 2018 23:37:35 +0900
Subject: [PATCH 227/286] wip

---
 src/web/app/desktop/script.ts                         |  8 ++++++++
 src/web/app/desktop/views/components/drive-window.vue |  2 +-
 src/web/app/desktop/views/components/window.vue       |  2 +-
 src/web/app/desktop/views/pages/drive.vue             | 11 +++++++++--
 4 files changed, 19 insertions(+), 4 deletions(-)

diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index 251a2a161..cf725d27c 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -18,6 +18,8 @@ import post from './api/post';
 
 import MkIndex from './views/pages/index.vue';
 import MkUser from './views/pages/user/user.vue';
+import MkSelectDrive from './views/pages/selectdrive.vue';
+import MkDrive from './views/pages/drive.vue';
 
 /**
  * init
@@ -58,6 +60,12 @@ init(async (launch) => {
 
 	app.$router.addRoutes([{
 		path: '/', component: MkIndex
+	}, {
+		path: '/i/drive', component: MkDrive
+	}, {
+		path: '/i/drive/folder/:folder', component: MkDrive
+	}, {
+		path: '/selectdrive', component: MkSelectDrive
 	}, {
 		path: '/:user', component: MkUser
 	}]);
diff --git a/src/web/app/desktop/views/components/drive-window.vue b/src/web/app/desktop/views/components/drive-window.vue
index 9fd5df830..8ae48cf39 100644
--- a/src/web/app/desktop/views/components/drive-window.vue
+++ b/src/web/app/desktop/views/components/drive-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" @closed="$destroy" width="800px" height="500px" :popout="popout">
+<mk-window ref="window" @closed="$destroy" width="800px" height="500px" :popout-url="popout">
 	<template slot="header">
 		<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:desktop.tags.mk-drive-browser-window.used%</p>
 		<span :class="$style.title">%fa:cloud%%i18n:desktop.tags.mk-drive-browser-window.drive%</span>
diff --git a/src/web/app/desktop/views/components/window.vue b/src/web/app/desktop/views/components/window.vue
index 7f7f77813..1dba9a25a 100644
--- a/src/web/app/desktop/views/components/window.vue
+++ b/src/web/app/desktop/views/components/window.vue
@@ -563,7 +563,7 @@ export default Vue.extend({
 						margin 0
 						padding 0
 						cursor pointer
-						font-size 1.2em
+						font-size 1em
 						color rgba(#000, 0.4)
 						border none
 						outline none
diff --git a/src/web/app/desktop/views/pages/drive.vue b/src/web/app/desktop/views/pages/drive.vue
index 3ce5af769..353f59b70 100644
--- a/src/web/app/desktop/views/pages/drive.vue
+++ b/src/web/app/desktop/views/pages/drive.vue
@@ -1,13 +1,20 @@
 <template>
 <div class="mk-drive-page">
-	<mk-drive :folder="folder" @move-root="onMoveRoot" @open-folder="onOpenFolder"/>
+	<mk-drive :init-folder="folder" @move-root="onMoveRoot" @open-folder="onOpenFolder"/>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
 export default Vue.extend({
-	props: ['folder'],
+	data() {
+		return {
+			folder: null
+		};
+	},
+	created() {
+		this.folder = this.$route.params.folder;
+	},
 	mounted() {
 		document.title = 'Misskey Drive';
 	},

From e4120e930c97f26d4bc344e9135588be094a0306 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 01:12:18 +0900
Subject: [PATCH 228/286] wip

---
 src/web/app/desktop/views/components/home.vue     | 6 +++---
 src/web/app/desktop/views/components/timeline.vue | 1 +
 src/web/app/desktop/views/pages/home.vue          | 6 +++++-
 3 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index e815239d3..8e64a2d83 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -10,15 +10,15 @@
 					<option value="calendar">カレンダー</option>
 					<option value="timemachine">カレンダー(タイムマシン)</option>
 					<option value="activity">アクティビティ</option>
-					<option value="rss-reader">RSSリーダー</option>
+					<option value="rss">RSSリーダー</option>
 					<option value="trends">トレンド</option>
 					<option value="photo-stream">フォトストリーム</option>
 					<option value="slideshow">スライドショー</option>
 					<option value="version">バージョン</option>
 					<option value="broadcast">ブロードキャスト</option>
 					<option value="notifications">通知</option>
-					<option value="user-recommendation">おすすめユーザー</option>
-					<option value="recommended-polls">投票</option>
+					<option value="users">おすすめユーザー</option>
+					<option value="polls">投票</option>
 					<option value="post-form">投稿フォーム</option>
 					<option value="messaging">メッセージ</option>
 					<option value="channel">チャンネル</option>
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index 875a7961e..a3f27412d 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -59,6 +59,7 @@ export default Vue.extend({
 			}).then(posts => {
 				this.posts = posts;
 				this.fetching = false;
+				this.$emit('loaded');
 				if (cb) cb();
 			});
 		},
diff --git a/src/web/app/desktop/views/pages/home.vue b/src/web/app/desktop/views/pages/home.vue
index e19b7fc8f..e1464bab1 100644
--- a/src/web/app/desktop/views/pages/home.vue
+++ b/src/web/app/desktop/views/pages/home.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-ui>
-	<mk-home :mode="mode"/>
+	<mk-home :mode="mode" @loaded="loaded"/>
 </mk-ui>
 </template>
 
@@ -40,6 +40,10 @@ export default Vue.extend({
 		document.removeEventListener('visibilitychange', this.onVisibilitychange);
 	},
 	methods: {
+		loaded() {
+			Progress.done();
+		},
+
 		onStreamPost(post) {
 			if (document.hidden && post.user_id != (this as any).os.i.id) {
 				this.unreadCount++;

From 49d257992fe5e2134498e65b5cf76942cf877aa9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 01:14:25 +0900
Subject: [PATCH 229/286] wip

---
 src/web/app/desktop/script.ts                          | 2 +-
 src/web/app/desktop/views/components/ui.header.nav.vue | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index cf725d27c..2477f62f4 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -59,7 +59,7 @@ init(async (launch) => {
 	}
 
 	app.$router.addRoutes([{
-		path: '/', component: MkIndex
+		path: '/', name: 'index', component: MkIndex
 	}, {
 		path: '/i/drive', component: MkDrive
 	}, {
diff --git a/src/web/app/desktop/views/components/ui.header.nav.vue b/src/web/app/desktop/views/components/ui.header.nav.vue
index 5895255ff..70c616d9c 100644
--- a/src/web/app/desktop/views/components/ui.header.nav.vue
+++ b/src/web/app/desktop/views/components/ui.header.nav.vue
@@ -2,7 +2,7 @@
 <div class="nav">
 	<ul>
 		<template v-if="os.isSignedIn">
-			<li class="home" :class="{ active: page == 'home' }">
+			<li class="home" :class="{ active: $route.name == 'index' }">
 				<router-link to="/">
 					%fa:home%
 					<p>%i18n:desktop.tags.mk-ui-header-nav.home%</p>

From 34f91f04ad5d3933452eaa257839fb3428b00360 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 01:23:25 +0900
Subject: [PATCH 230/286] wip

---
 src/web/app/common/views/components/poll-editor.vue | 2 +-
 src/web/app/desktop/views/components/drive.vue      | 4 ++--
 src/web/app/mobile/views/components/drive.vue       | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/web/app/common/views/components/poll-editor.vue b/src/web/app/common/views/components/poll-editor.vue
index 2ae91bf25..7428d8054 100644
--- a/src/web/app/common/views/components/poll-editor.vue
+++ b/src/web/app/common/views/components/poll-editor.vue
@@ -28,7 +28,7 @@ export default Vue.extend({
 	},
 	methods: {
 		onInput(i, e) {
-			this.choices[i] = e.target.value; // TODO
+			Vue.set(this.choices, i, e.target.value);
 		},
 
 		add() {
diff --git a/src/web/app/desktop/views/components/drive.vue b/src/web/app/desktop/views/components/drive.vue
index 2b33265e5..fd30e1359 100644
--- a/src/web/app/desktop/views/components/drive.vue
+++ b/src/web/app/desktop/views/components/drive.vue
@@ -417,7 +417,7 @@ export default Vue.extend({
 
 			if (this.folders.some(f => f.id == folder.id)) {
 				const exist = this.folders.map(f => f.id).indexOf(folder.id);
-				this.folders[exist] = folder; // TODO
+				Vue.set(this.folders, exist, folder);
 				return;
 			}
 
@@ -434,7 +434,7 @@ export default Vue.extend({
 
 			if (this.files.some(f => f.id == file.id)) {
 				const exist = this.files.map(f => f.id).indexOf(file.id);
-				this.files[exist] = file; // TODO
+				Vue.set(this.files, exist, file);
 				return;
 			}
 
diff --git a/src/web/app/mobile/views/components/drive.vue b/src/web/app/mobile/views/components/drive.vue
index 59b2c256d..f334f2241 100644
--- a/src/web/app/mobile/views/components/drive.vue
+++ b/src/web/app/mobile/views/components/drive.vue
@@ -193,7 +193,7 @@ export default Vue.extend({
 
 			if (this.files.some(f => f.id == file.id)) {
 				const exist = this.files.map(f => f.id).indexOf(file.id);
-				this.files[exist] = file; // TODO
+				Vue.set(this.files, exist, file);
 				return;
 			}
 

From b2a6257f93612ed59a00548feff4712a5136faab Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 01:39:51 +0900
Subject: [PATCH 231/286] wip

---
 src/web/app/auth/views/index.vue              |  6 ++--
 src/web/app/common/views/components/index.ts  |  4 ---
 ...aging-form.vue => messaging-room.form.vue} |  0
 ...message.vue => messaging-room.message.vue} | 12 +++----
 .../views/components/messaging-room.vue       | 14 ++++++--
 .../app/desktop/views/components/activity.vue | 12 +++----
 .../views/components/context-menu.menu.vue    |  2 +-
 .../desktop/views/components/context-menu.vue |  6 ++--
 .../app/desktop/views/components/drive.vue    |  6 ++--
 .../desktop/views/components/post-detail.vue  |  2 +-
 .../desktop/views/components/posts.post.vue   |  2 +-
 .../app/desktop/views/components/posts.vue    |  7 ++--
 .../app/desktop/views/components/settings.vue |  2 +-
 .../desktop/views/components/ui.header.vue    | 12 +++----
 src/web/app/desktop/views/components/ui.vue   |  2 +-
 .../views/components/widgets/server.cpu.vue   |  2 +-
 .../views/components/widgets/server.disk.vue  |  2 +-
 .../components/widgets/server.memory.vue      |  2 +-
 .../views/components/widgets/server.vue       | 12 +++----
 src/web/app/desktop/views/pages/index.vue     | 10 +++---
 ...u-know.vue => user.followers-you-know.vue} |  4 +--
 .../{user-friends.vue => user.friends.vue}    |  4 +--
 .../user/{user-header.vue => user.header.vue} |  4 +--
 .../user/{user-home.vue => user.home.vue}     | 34 +++++++++----------
 .../user/{user-photos.vue => user.photos.vue} |  4 +--
 .../{user-profile.vue => user.profile.vue}    |  4 +--
 .../{user-timeline.vue => user.timeline.vue}  |  4 +--
 src/web/app/desktop/views/pages/user/user.vue | 13 ++++---
 .../mobile/views/components/notifications.vue |  2 +-
 src/web/app/mobile/views/components/posts.vue |  2 +-
 .../mobile/views/pages/user/home-activity.vue |  2 +-
 31 files changed, 100 insertions(+), 94 deletions(-)
 rename src/web/app/common/views/components/{messaging-form.vue => messaging-room.form.vue} (100%)
 rename src/web/app/common/views/components/{messaging-message.vue => messaging-room.message.vue} (96%)
 rename src/web/app/desktop/views/pages/user/{user-followers-you-know.vue => user.followers-you-know.vue} (95%)
 rename src/web/app/desktop/views/pages/user/{user-friends.vue => user.friends.vue} (98%)
 rename src/web/app/desktop/views/pages/user/{user-header.vue => user.header.vue} (97%)
 rename src/web/app/desktop/views/pages/user/{user-home.vue => user.home.vue} (65%)
 rename src/web/app/desktop/views/pages/user/{user-photos.vue => user.photos.vue} (97%)
 rename src/web/app/desktop/views/pages/user/{user-profile.vue => user.profile.vue} (98%)
 rename src/web/app/desktop/views/pages/user/{user-timeline.vue => user.timeline.vue} (98%)

diff --git a/src/web/app/auth/views/index.vue b/src/web/app/auth/views/index.vue
index 56a7bac7a..1e372c0bd 100644
--- a/src/web/app/auth/views/index.vue
+++ b/src/web/app/auth/views/index.vue
@@ -2,7 +2,7 @@
 <div class="index">
 	<main v-if="os.isSignedIn">
 		<p class="fetching" v-if="fetching">読み込み中<mk-ellipsis/></p>
-		<fo-rm
+		<x-form
 			ref="form"
 			v-if="state == 'waiting'"
 			:session="session"
@@ -32,11 +32,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import Form from './form.vue';
+import XForm from './form.vue';
 
 export default Vue.extend({
 	components: {
-		'fo-rm': Form
+		XForm
 	},
 	data() {
 		return {
diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index 021e45a8d..a61022dbe 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -15,9 +15,7 @@ import specialMessage from './special-message.vue';
 import streamIndicator from './stream-indicator.vue';
 import ellipsis from './ellipsis.vue';
 import messaging from './messaging.vue';
-import messagingForm from './messaging-form.vue';
 import messagingRoom from './messaging-room.vue';
-import messagingMessage from './messaging-message.vue';
 import urlPreview from './url-preview.vue';
 
 Vue.component('mk-signin', signin);
@@ -35,7 +33,5 @@ Vue.component('mk-special-message', specialMessage);
 Vue.component('mk-stream-indicator', streamIndicator);
 Vue.component('mk-ellipsis', ellipsis);
 Vue.component('mk-messaging', messaging);
-Vue.component('mk-messaging-form', messagingForm);
 Vue.component('mk-messaging-room', messagingRoom);
-Vue.component('mk-messaging-message', messagingMessage);
 Vue.component('mk-url-preview', urlPreview);
diff --git a/src/web/app/common/views/components/messaging-form.vue b/src/web/app/common/views/components/messaging-room.form.vue
similarity index 100%
rename from src/web/app/common/views/components/messaging-form.vue
rename to src/web/app/common/views/components/messaging-room.form.vue
diff --git a/src/web/app/common/views/components/messaging-message.vue b/src/web/app/common/views/components/messaging-room.message.vue
similarity index 96%
rename from src/web/app/common/views/components/messaging-message.vue
rename to src/web/app/common/views/components/messaging-room.message.vue
index d2e3dacb5..95a6efa28 100644
--- a/src/web/app/common/views/components/messaging-message.vue
+++ b/src/web/app/common/views/components/messaging-room.message.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-messaging-message" :data-is-me="isMe">
+<div class="message" :data-is-me="isMe">
 	<a class="avatar-anchor" :href="`/${message.user.username}`" :title="message.user.username" target="_blank">
 		<img class="avatar" :src="`${message.user.avatar_url}?thumbnail&size=80`" alt=""/>
 	</a>
@@ -51,7 +51,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-messaging-message
+.message
 	$me-balloon-color = #23A7B6
 
 	padding 10px 12px 10px 12px
@@ -181,7 +181,7 @@ export default Vue.extend({
 			> [data-fa]
 				margin-left 4px
 
-	&:not([data-is-me='true'])
+	&:not([data-is-me])
 		> .avatar-anchor
 			float left
 
@@ -201,7 +201,7 @@ export default Vue.extend({
 			> footer
 				text-align left
 
-	&[data-is-me='true']
+	&[data-is-me]
 		> .avatar-anchor
 			float right
 
@@ -224,14 +224,14 @@ export default Vue.extend({
 					> p.is-deleted
 						color rgba(255, 255, 255, 0.5)
 
-					> [ref='text']
+					> .text
 						&, *
 							color #fff !important
 
 			> footer
 				text-align right
 
-	&[data-is-deleted='true']
+	&[data-is-deleted]
 			> .content-container
 				opacity 0.5
 
diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index d03799563..5022655a2 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -8,14 +8,16 @@
 			<template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:common.tags.mk-messaging-room.more%' }}
 		</button>
 		<template v-for="(message, i) in _messages">
-			<mk-messaging-message :message="message" :key="message.id"/>
-			<p class="date" :key="message.id + '-time'" v-if="i != messages.length - 1 && message._date != _messages[i + 1]._date"><span>{{ _messages[i + 1]._datetext }}</span></p>
+			<x-message :message="message" :key="message.id"/>
+			<p class="date" v-if="i != messages.length - 1 && message._date != _messages[i + 1]._date">
+				<span>{{ _messages[i + 1]._datetext }}</span>
+			</p>
 		</template>
 	</div>
 	<footer>
 		<div ref="notifications"></div>
 		<div class="grippie" title="%i18n:common.tags.mk-messaging-room.resize-form%"></div>
-		<mk-messaging-form :user="user"/>
+		<x-form :user="user"/>
 	</footer>
 </div>
 </template>
@@ -23,8 +25,14 @@
 <script lang="ts">
 import Vue from 'vue';
 import MessagingStreamConnection from '../../scripts/streaming/messaging-stream';
+import XMessage from './messaging-room.message.vue';
+import XForm from './messaging-room.form.vue';
 
 export default Vue.extend({
+	components: {
+		XMessage,
+		XForm
+	},
 	props: ['user', 'isNaked'],
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/activity.vue b/src/web/app/desktop/views/components/activity.vue
index d1c44f0f5..1b2cc9afd 100644
--- a/src/web/app/desktop/views/components/activity.vue
+++ b/src/web/app/desktop/views/components/activity.vue
@@ -6,21 +6,21 @@
 	</template>
 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<template v-else>
-		<mk-activity-widget-calender v-show="view == 0" :data="[].concat(activity)"/>
-		<mk-activity-widget-chart v-show="view == 1" :data="[].concat(activity)"/>
+		<x-calender v-show="view == 0" :data="[].concat(activity)"/>
+		<x-chart v-show="view == 1" :data="[].concat(activity)"/>
 	</template>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import Calendar from './activity.calendar.vue';
-import Chart from './activity.chart.vue';
+import XCalendar from './activity.calendar.vue';
+import XChart from './activity.chart.vue';
 
 export default Vue.extend({
 	components: {
-		'mk-activity-widget-calender': Calendar,
-		'mk-activity-widget-chart': Chart
+		XCalendar,
+		XChart
 	},
 	props: {
 		design: {
diff --git a/src/web/app/desktop/views/components/context-menu.menu.vue b/src/web/app/desktop/views/components/context-menu.menu.vue
index 317833d9a..e2c34a591 100644
--- a/src/web/app/desktop/views/components/context-menu.menu.vue
+++ b/src/web/app/desktop/views/components/context-menu.menu.vue
@@ -1,6 +1,6 @@
 <template>
 <ul class="menu">
-	<li v-for="(item, i) in menu" :key="i" :class="item.type">
+	<li v-for="(item, i) in menu" :class="item.type">
 		<template v-if="item.type == 'item'">
 			<p @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</p>
 		</template>
diff --git a/src/web/app/desktop/views/components/context-menu.vue b/src/web/app/desktop/views/components/context-menu.vue
index 6076cdeb9..8bd994584 100644
--- a/src/web/app/desktop/views/components/context-menu.vue
+++ b/src/web/app/desktop/views/components/context-menu.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="context-menu" :style="{ left: `${x}px`, top: `${y}px` }" @contextmenu.prevent="() => {}">
-	<me-nu :menu="menu" @x="click"/>
+	<x-menu :menu="menu" @x="click"/>
 </div>
 </template>
 
@@ -8,11 +8,11 @@
 import Vue from 'vue';
 import * as anime from 'animejs';
 import contains from '../../../common/scripts/contains';
-import meNu from './context-menu.menu.vue';
+import XMenu from './context-menu.menu.vue';
 
 export default Vue.extend({
 	components: {
-		'me-nu': meNu
+		XMenu
 	},
 	props: ['x', 'y', 'menu'],
 	mounted() {
diff --git a/src/web/app/desktop/views/components/drive.vue b/src/web/app/desktop/views/components/drive.vue
index fd30e1359..064e4de66 100644
--- a/src/web/app/desktop/views/components/drive.vue
+++ b/src/web/app/desktop/views/components/drive.vue
@@ -4,7 +4,7 @@
 		<div class="path" @contextmenu.prevent.stop="() => {}">
 			<mk-drive-nav-folder :class="{ current: folder == null }"/>
 			<template v-for="folder in hierarchyFolders">
-				<span class="separator" :key="folder.id + '>'">%fa:angle-right%</span>
+				<span class="separator">%fa:angle-right%</span>
 				<mk-drive-nav-folder :folder="folder" :key="folder.id"/>
 			</template>
 			<span class="separator" v-if="folder != null">%fa:angle-right%</span>
@@ -26,13 +26,13 @@
 			<div class="folders" ref="foldersContainer" v-if="folders.length > 0">
 				<mk-drive-folder v-for="folder in folders" :key="folder.id" class="folder" :folder="folder"/>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
-				<div class="padding" v-for="n in 16" :key="n"></div>
+				<div class="padding" v-for="n in 16"></div>
 				<button v-if="moreFolders">%i18n:desktop.tags.mk-drive-browser.load-more%</button>
 			</div>
 			<div class="files" ref="filesContainer" v-if="files.length > 0">
 				<mk-drive-file v-for="file in files" :key="file.id" class="file" :file="file"/>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
-				<div class="padding" v-for="n in 16" :key="n"></div>
+				<div class="padding" v-for="n in 16"></div>
 				<button v-if="moreFiles" @click="fetchMoreFiles">%i18n:desktop.tags.mk-drive-browser.load-more%</button>
 			</div>
 			<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index c9fe00fca..6eca03520 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -79,7 +79,7 @@ import XSub from './post-detail.sub.vue';
 
 export default Vue.extend({
 	components: {
-		'x-sub': XSub
+		XSub
 	},
 	props: {
 		post: {
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 05934571a..92218ead3 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -95,7 +95,7 @@ function focus(el, fn) {
 
 export default Vue.extend({
 	components: {
-		'x-sub': XSub
+		XSub
 	},
 	props: ['post'],
 	data() {
diff --git a/src/web/app/desktop/views/components/posts.vue b/src/web/app/desktop/views/components/posts.vue
index bda24e143..7576fd31b 100644
--- a/src/web/app/desktop/views/components/posts.vue
+++ b/src/web/app/desktop/views/components/posts.vue
@@ -2,7 +2,10 @@
 <div class="mk-posts">
 	<template v-for="(post, i) in _posts">
 		<x-post :post.sync="post" :key="post.id"/>
-		<p class="date" :key="post.id + '-time'" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date"><span>%fa:angle-up%{{ post._datetext }}</span><span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span></p>
+		<p class="date" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date">
+			<span>%fa:angle-up%{{ post._datetext }}</span>
+			<span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span>
+		</p>
 	</template>
 	<footer>
 		<slot name="footer"></slot>
@@ -16,7 +19,7 @@ import XPost from './posts.post.vue';
 
 export default Vue.extend({
 	components: {
-		'x-post': XPost
+		XPost
 	},
 	props: {
 		posts: {
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 681e373ed..148e11ed2 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -77,7 +77,7 @@ import XProfile from './settings.profile.vue';
 
 export default Vue.extend({
 	components: {
-		'x-profile': XProfile
+		XProfile
 	},
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/ui.header.vue b/src/web/app/desktop/views/components/ui.header.vue
index ef5e3a95d..99de05fac 100644
--- a/src/web/app/desktop/views/components/ui.header.vue
+++ b/src/web/app/desktop/views/components/ui.header.vue
@@ -33,12 +33,12 @@ import XClock from './ui.header.clock.vue';
 
 export default Vue.extend({
 	components: {
-		'x-nav': XNav,
-		'x-search': XSearch,
-		'x-account': XAccount,
-		'x-notifications': XNotifications,
-		'x-post': XPost,
-		'x-clock': XClock,
+		XNav,
+		XSearch,
+		XAccount,
+		XNotifications,
+		XPost,
+		XClock,
 	}
 });
 </script>
diff --git a/src/web/app/desktop/views/components/ui.vue b/src/web/app/desktop/views/components/ui.vue
index 9cd12f964..87f932ff1 100644
--- a/src/web/app/desktop/views/components/ui.vue
+++ b/src/web/app/desktop/views/components/ui.vue
@@ -14,7 +14,7 @@ import XHeader from './ui.header.vue';
 
 export default Vue.extend({
 	components: {
-		'x-header': XHeader
+		XHeader
 	},
 	mounted() {
 		document.addEventListener('keydown', this.onKeydown);
diff --git a/src/web/app/desktop/views/components/widgets/server.cpu.vue b/src/web/app/desktop/views/components/widgets/server.cpu.vue
index 337ff62ce..96184d188 100644
--- a/src/web/app/desktop/views/components/widgets/server.cpu.vue
+++ b/src/web/app/desktop/views/components/widgets/server.cpu.vue
@@ -15,7 +15,7 @@ import XPie from './server.pie.vue';
 
 export default Vue.extend({
 	components: {
-		'x-pie': XPie
+		XPie
 	},
 	props: ['connection', 'meta'],
 	data() {
diff --git a/src/web/app/desktop/views/components/widgets/server.disk.vue b/src/web/app/desktop/views/components/widgets/server.disk.vue
index c21c56290..2af1982a9 100644
--- a/src/web/app/desktop/views/components/widgets/server.disk.vue
+++ b/src/web/app/desktop/views/components/widgets/server.disk.vue
@@ -16,7 +16,7 @@ import XPie from './server.pie.vue';
 
 export default Vue.extend({
 	components: {
-		'x-pie': XPie
+		XPie
 	},
 	props: ['connection'],
 	data() {
diff --git a/src/web/app/desktop/views/components/widgets/server.memory.vue b/src/web/app/desktop/views/components/widgets/server.memory.vue
index 2afc627fd..834a62671 100644
--- a/src/web/app/desktop/views/components/widgets/server.memory.vue
+++ b/src/web/app/desktop/views/components/widgets/server.memory.vue
@@ -16,7 +16,7 @@ import XPie from './server.pie.vue';
 
 export default Vue.extend({
 	components: {
-		'x-pie': XPie
+		XPie
 	},
 	props: ['connection'],
 	data() {
diff --git a/src/web/app/desktop/views/components/widgets/server.vue b/src/web/app/desktop/views/components/widgets/server.vue
index 5aa01fd4e..00e2f8f18 100644
--- a/src/web/app/desktop/views/components/widgets/server.vue
+++ b/src/web/app/desktop/views/components/widgets/server.vue
@@ -33,12 +33,12 @@ export default define({
 	}
 }).extend({
 	components: {
-		'x-cpu-and-memory': XCpuMemory,
-		'x-cpu': XCpu,
-		'x-memory': XMemory,
-		'x-disk': XDisk,
-		'x-uptimes': XUptimes,
-		'x-info': XInfo
+		XCpuMemory,
+		XCpu,
+		XMemory,
+		XDisk,
+		XUptimes,
+		XInfo
 	},
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/pages/index.vue b/src/web/app/desktop/views/pages/index.vue
index bd32c17b3..6b8739e30 100644
--- a/src/web/app/desktop/views/pages/index.vue
+++ b/src/web/app/desktop/views/pages/index.vue
@@ -1,16 +1,16 @@
 <template>
-	<component v-bind:is="os.isSignedIn ? 'home' : 'welcome'"></component>
+	<component :is="os.isSignedIn ? 'home' : 'welcome'"></component>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import HomeView from './home.vue';
-import WelcomeView from './welcome.vue';
+import Home from './home.vue';
+import Welcome from './welcome.vue';
 
 export default Vue.extend({
 	components: {
-		home: HomeView,
-		welcome: WelcomeView
+		Home,
+		Welcome
 	}
 });
 </script>
diff --git a/src/web/app/desktop/views/pages/user/user-followers-you-know.vue b/src/web/app/desktop/views/pages/user/user.followers-you-know.vue
similarity index 95%
rename from src/web/app/desktop/views/pages/user/user-followers-you-know.vue
rename to src/web/app/desktop/views/pages/user/user.followers-you-know.vue
index 6f6673a7a..015b12d3d 100644
--- a/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
+++ b/src/web/app/desktop/views/pages/user/user.followers-you-know.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-user-followers-you-know">
+<div class="followers-you-know">
 	<p class="title">%fa:users%%i18n:desktop.tags.mk-user.followers-you-know.title%</p>
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p>
 	<div v-if="!fetching && users.length > 0">
@@ -35,7 +35,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-user-followers-you-know
+.followers-you-know
 	background #fff
 	border solid 1px rgba(0, 0, 0, 0.075)
 	border-radius 6px
diff --git a/src/web/app/desktop/views/pages/user/user-friends.vue b/src/web/app/desktop/views/pages/user/user.friends.vue
similarity index 98%
rename from src/web/app/desktop/views/pages/user/user-friends.vue
rename to src/web/app/desktop/views/pages/user/user.friends.vue
index b173e4296..d27009a82 100644
--- a/src/web/app/desktop/views/pages/user/user-friends.vue
+++ b/src/web/app/desktop/views/pages/user/user.friends.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-user-friends">
+<div class="friends">
 	<p class="title">%fa:users%%i18n:desktop.tags.mk-user.frequently-replied-users.title%</p>
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p>
 	<template v-if="!fetching && users.length != 0">
@@ -41,7 +41,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-user-friends
+.friends
 	background #fff
 	border solid 1px rgba(0, 0, 0, 0.075)
 	border-radius 6px
diff --git a/src/web/app/desktop/views/pages/user/user-header.vue b/src/web/app/desktop/views/pages/user/user.header.vue
similarity index 97%
rename from src/web/app/desktop/views/pages/user/user-header.vue
rename to src/web/app/desktop/views/pages/user/user.header.vue
index b4a24459c..81174f657 100644
--- a/src/web/app/desktop/views/pages/user/user-header.vue
+++ b/src/web/app/desktop/views/pages/user/user.header.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-user-header" :data-is-dark-background="user.banner_url != null">
+<div class="header" :data-is-dark-background="user.banner_url != null">
 	<div class="banner-container" :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=2048)` : ''">
 		<div class="banner" ref="banner" :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=2048)` : ''" @click="onBannerClick"></div>
 	</div>
@@ -62,7 +62,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-user-header
+.header
 	$banner-height = 320px
 	$footer-height = 58px
 
diff --git a/src/web/app/desktop/views/pages/user/user-home.vue b/src/web/app/desktop/views/pages/user/user.home.vue
similarity index 65%
rename from src/web/app/desktop/views/pages/user/user-home.vue
rename to src/web/app/desktop/views/pages/user/user.home.vue
index 5ed901579..bf96741cb 100644
--- a/src/web/app/desktop/views/pages/user/user-home.vue
+++ b/src/web/app/desktop/views/pages/user/user.home.vue
@@ -1,22 +1,22 @@
 <template>
-<div class="mk-user-home">
+<div class="home">
 	<div>
 		<div ref="left">
-			<mk-user-profile :user="user"/>
-			<mk-user-photos :user="user"/>
-			<mk-user-followers-you-know v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
+			<x-profile :user="user"/>
+			<x-photos :user="user"/>
+			<x-followers-you-know v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
 			<p>%i18n:desktop.tags.mk-user.last-used-at%: <b><mk-time :time="user.last_used_at"/></b></p>
 		</div>
 	</div>
 	<main>
 		<mk-post-detail v-if="user.pinned_post" :post="user.pinned_post" compact/>
-		<mk-user-timeline ref="tl" :user="user"/>
+		<x-timeline ref="tl" :user="user"/>
 	</main>
 	<div>
 		<div ref="right">
 			<mk-calendar @chosen="warp" :start="new Date(user.created_at)"/>
 			<mk-activity :user="user"/>
-			<mk-user-friends :user="user"/>
+			<x-friends :user="user"/>
 			<div class="nav"><mk-nav/></div>
 		</div>
 	</div>
@@ -25,19 +25,19 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import MkUserTimeline from './user-timeline.vue';
-import MkUserProfile from './user-profile.vue';
-import MkUserPhotos from './user-photos.vue';
-import MkUserFollowersYouKnow from './user-followers-you-know.vue';
-import MkUserFriends from './user-friends.vue';
+import XUserTimeline from './user.timeline.vue';
+import XUserProfile from './user.profile.vue';
+import XUserPhotos from './user.photos.vue';
+import XUserFollowersYouKnow from './user.followers-you-know.vue';
+import XUserFriends from './user.friends.vue';
 
 export default Vue.extend({
 	components: {
-		'mk-user-timeline': MkUserTimeline,
-		'mk-user-profile': MkUserProfile,
-		'mk-user-photos': MkUserPhotos,
-		'mk-user-followers-you-know': MkUserFollowersYouKnow,
-		'mk-user-friends': MkUserFriends
+		XUserTimeline,
+		XUserProfile,
+		XUserPhotos,
+		XUserFollowersYouKnow,
+		XUserFriends
 	},
 	props: ['user'],
 	methods: {
@@ -49,7 +49,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-user-home
+.home
 	display flex
 	justify-content center
 	margin 0 auto
diff --git a/src/web/app/desktop/views/pages/user/user-photos.vue b/src/web/app/desktop/views/pages/user/user.photos.vue
similarity index 97%
rename from src/web/app/desktop/views/pages/user/user-photos.vue
rename to src/web/app/desktop/views/pages/user/user.photos.vue
index 4029a95cc..db29a9945 100644
--- a/src/web/app/desktop/views/pages/user/user-photos.vue
+++ b/src/web/app/desktop/views/pages/user/user.photos.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-user-photos">
+<div class="photos">
 	<p class="title">%fa:camera%%i18n:desktop.tags.mk-user.photos.title%</p>
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.photos.loading%<mk-ellipsis/></p>
 	<div class="stream" v-if="!fetching && images.length > 0">
@@ -39,7 +39,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-user-photos
+.photos
 	background #fff
 	border solid 1px rgba(0, 0, 0, 0.075)
 	border-radius 6px
diff --git a/src/web/app/desktop/views/pages/user/user-profile.vue b/src/web/app/desktop/views/pages/user/user.profile.vue
similarity index 98%
rename from src/web/app/desktop/views/pages/user/user-profile.vue
rename to src/web/app/desktop/views/pages/user/user.profile.vue
index 32c28595e..db2e32e80 100644
--- a/src/web/app/desktop/views/pages/user/user-profile.vue
+++ b/src/web/app/desktop/views/pages/user/user.profile.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-user-profile">
+<div class="profile">
 	<div class="friend-form" v-if="os.isSignedIn && os.i.id != user.id">
 		<mk-follow-button :user="user" size="big"/>
 		<p class="followed" v-if="user.is_followed">%i18n:desktop.tags.mk-user.follows-you%</p>
@@ -75,7 +75,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-user-profile
+.profile
 	background #fff
 	border solid 1px rgba(0, 0, 0, 0.075)
 	border-radius 6px
diff --git a/src/web/app/desktop/views/pages/user/user-timeline.vue b/src/web/app/desktop/views/pages/user/user.timeline.vue
similarity index 98%
rename from src/web/app/desktop/views/pages/user/user-timeline.vue
rename to src/web/app/desktop/views/pages/user/user.timeline.vue
index 9dd07653c..51c7589fd 100644
--- a/src/web/app/desktop/views/pages/user/user-timeline.vue
+++ b/src/web/app/desktop/views/pages/user/user.timeline.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-user-timeline">
+<div class="timeline">
 	<header>
 		<span :data-is-active="mode == 'default'" @click="mode = 'default'">投稿</span>
 		<span :data-is-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿と返信</span>
@@ -93,7 +93,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-user-timeline
+.timeline
 	background #fff
 
 	> header
diff --git a/src/web/app/desktop/views/pages/user/user.vue b/src/web/app/desktop/views/pages/user/user.vue
index 84f31e854..095df0e48 100644
--- a/src/web/app/desktop/views/pages/user/user.vue
+++ b/src/web/app/desktop/views/pages/user/user.vue
@@ -1,9 +1,8 @@
 <template>
 <mk-ui>
 	<div class="user" v-if="!fetching">
-		<mk-user-header :user="user"/>
-		<mk-user-home v-if="page == 'home'" :user="user"/>
-		<mk-user-graphs v-if="page == 'graphs'" :user="user"/>
+		<x-header :user="user"/>
+		<x-home v-if="page == 'home'" :user="user"/>
 	</div>
 </mk-ui>
 </template>
@@ -11,13 +10,13 @@
 <script lang="ts">
 import Vue from 'vue';
 import Progress from '../../../../common/scripts/loading';
-import MkUserHeader from './user-header.vue';
-import MkUserHome from './user-home.vue';
+import XHeader from './user.header.vue';
+import XHome from './user.home.vue';
 
 export default Vue.extend({
 	components: {
-		'mk-user-header': MkUserHeader,
-		'mk-user-home': MkUserHome
+		XHeader,
+		XHome
 	},
 	props: {
 		page: {
diff --git a/src/web/app/mobile/views/components/notifications.vue b/src/web/app/mobile/views/components/notifications.vue
index 999dba404..cc4b743ac 100644
--- a/src/web/app/mobile/views/components/notifications.vue
+++ b/src/web/app/mobile/views/components/notifications.vue
@@ -3,7 +3,7 @@
 	<div class="notifications" v-if="notifications.length != 0">
 		<template v-for="(notification, i) in _notifications">
 			<mk-notification :notification="notification" :key="notification.id"/>
-			<p class="date" :key="notification.id + '-time'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date">
+			<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date">
 				<span>%fa:angle-up%{ notification._datetext }</span>
 				<span>%fa:angle-down%{ _notifications[i + 1]._datetext }</span>
 			</p>
diff --git a/src/web/app/mobile/views/components/posts.vue b/src/web/app/mobile/views/components/posts.vue
index 0edda5e94..e3abd9ca6 100644
--- a/src/web/app/mobile/views/components/posts.vue
+++ b/src/web/app/mobile/views/components/posts.vue
@@ -3,7 +3,7 @@
 	<slot name="head"></slot>
 	<template v-for="(post, i) in _posts">
 		<mk-posts-post :post="post" :key="post.id"/>
-		<p class="date" :key="post._datetext" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date">
+		<p class="date" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date">
 			<span>%fa:angle-up%{{ post._datetext }}</span>
 			<span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span>
 		</p>
diff --git a/src/web/app/mobile/views/pages/user/home-activity.vue b/src/web/app/mobile/views/pages/user/home-activity.vue
index f38c5568e..87c1dca89 100644
--- a/src/web/app/mobile/views/pages/user/home-activity.vue
+++ b/src/web/app/mobile/views/pages/user/home-activity.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-user-home-activity">
 	<svg v-if="data" ref="canvas" viewBox="0 0 30 1" preserveAspectRatio="none">
-		<g v-for="(d, i) in data.reverse()" :key="i">
+		<g v-for="(d, i) in data.reverse()">
 			<rect width="0.8" :height="d.postsH"
 				:x="i + 0.1" :y="1 - d.postsH - d.repliesH - d.repostsH"
 				fill="#41ddde"/>

From a1e57841e71e442b1bf52e445e6c541f16ec066d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 02:53:34 +0900
Subject: [PATCH 232/286] wip

---
 .eslintrc                                     |  3 +-
 src/web/app/common/mios.ts                    | 34 +++++++
 src/web/app/common/scripts/fuck-ad-block.ts   | 21 ++++
 src/web/app/desktop/api/notify.ts             | 10 ++
 src/web/app/desktop/api/update-avatar.ts      | 95 +++++++++++++++++++
 src/web/app/desktop/api/update-banner.ts      | 95 +++++++++++++++++++
 src/web/app/desktop/script.ts                 | 24 +++--
 src/web/app/desktop/scripts/dialog.ts         | 16 ----
 src/web/app/desktop/scripts/fuck-ad-block.ts  | 20 ----
 src/web/app/desktop/scripts/input-dialog.ts   | 12 ---
 .../scripts/not-implemented-exception.ts      |  8 --
 src/web/app/desktop/scripts/notify.ts         |  8 --
 .../app/desktop/scripts/scroll-follower.ts    | 61 ------------
 src/web/app/desktop/scripts/update-avatar.ts  | 88 -----------------
 src/web/app/desktop/scripts/update-banner.ts  | 88 -----------------
 .../app/desktop/views/components/dialog.vue   |  2 +-
 .../desktop/views/components/post-form.vue    |  5 +-
 .../desktop/views/components/repost-form.vue  |  5 +-
 .../views/components/settings.profile.vue     |  3 +-
 .../views/components/ui-notification.vue      | 32 ++++---
 .../desktop/views/pages/user/user.header.vue  |  3 +-
 src/web/app/init.ts                           | 43 ++-------
 22 files changed, 304 insertions(+), 372 deletions(-)
 create mode 100644 src/web/app/common/scripts/fuck-ad-block.ts
 create mode 100644 src/web/app/desktop/api/notify.ts
 create mode 100644 src/web/app/desktop/api/update-avatar.ts
 create mode 100644 src/web/app/desktop/api/update-banner.ts
 delete mode 100644 src/web/app/desktop/scripts/dialog.ts
 delete mode 100644 src/web/app/desktop/scripts/fuck-ad-block.ts
 delete mode 100644 src/web/app/desktop/scripts/input-dialog.ts
 delete mode 100644 src/web/app/desktop/scripts/not-implemented-exception.ts
 delete mode 100644 src/web/app/desktop/scripts/notify.ts
 delete mode 100644 src/web/app/desktop/scripts/scroll-follower.ts
 delete mode 100644 src/web/app/desktop/scripts/update-avatar.ts
 delete mode 100644 src/web/app/desktop/scripts/update-banner.ts

diff --git a/.eslintrc b/.eslintrc
index 6caf8f532..679d4f12d 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -13,6 +13,7 @@
 		"vue/html-self-closing": false,
 		"vue/no-unused-vars": false,
 		"no-console": 0,
-		"no-unused-vars": 0
+		"no-unused-vars": 0,
+		"no-empty": 0
 	}
 }
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index a98df1bc0..c4208aa91 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -16,6 +16,38 @@ declare const _API_URL_: string;
 declare const _SW_PUBLICKEY_: string;
 //#endregion
 
+export type API = {
+	chooseDriveFile: (opts: {
+		title?: string;
+		currentFolder?: any;
+		multiple?: boolean;
+	}) => Promise<any>;
+
+	chooseDriveFolder: (opts: {
+		title?: string;
+		currentFolder?: any;
+	}) => Promise<any>;
+
+	dialog: (opts: {
+		title: string;
+		text: string;
+		actions: Array<{
+			text: string;
+			id?: string;
+		}>;
+	}) => Promise<string>;
+
+	input: (opts: {
+		title: string;
+		placeholder?: string;
+		default?: string;
+	}) => Promise<string>;
+
+	post: () => void;
+
+	notify: (message: string) => void;
+};
+
 /**
  * Misskey Operating System
  */
@@ -49,6 +81,8 @@ export default class MiOS extends EventEmitter {
 		return localStorage.getItem('debug') == 'true';
 	}
 
+	public apis: API;
+
 	/**
 	 * A connection manager of home stream
 	 */
diff --git a/src/web/app/common/scripts/fuck-ad-block.ts b/src/web/app/common/scripts/fuck-ad-block.ts
new file mode 100644
index 000000000..9bcf7deef
--- /dev/null
+++ b/src/web/app/common/scripts/fuck-ad-block.ts
@@ -0,0 +1,21 @@
+require('fuckadblock');
+
+declare const fuckAdBlock: any;
+
+export default (os) => {
+	function adBlockDetected() {
+		os.apis.dialog({
+			title: '%fa:exclamation-triangle%広告ブロッカーを無効にしてください',
+			text: '<strong>Misskeyは広告を掲載していません</strong>が、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。',
+			actins: [{
+				text: 'OK'
+			}]
+		});
+	}
+
+	if (fuckAdBlock === undefined) {
+		adBlockDetected();
+	} else {
+		fuckAdBlock.onDetected(adBlockDetected);
+	}
+};
diff --git a/src/web/app/desktop/api/notify.ts b/src/web/app/desktop/api/notify.ts
new file mode 100644
index 000000000..1f89f40ce
--- /dev/null
+++ b/src/web/app/desktop/api/notify.ts
@@ -0,0 +1,10 @@
+import Notification from '../views/components/ui-notification.vue';
+
+export default function(message) {
+	const vm = new Notification({
+		propsData: {
+			message
+		}
+	}).$mount();
+	document.body.appendChild(vm.$el);
+}
diff --git a/src/web/app/desktop/api/update-avatar.ts b/src/web/app/desktop/api/update-avatar.ts
new file mode 100644
index 000000000..eff072834
--- /dev/null
+++ b/src/web/app/desktop/api/update-avatar.ts
@@ -0,0 +1,95 @@
+import OS from '../../common/mios';
+import { apiUrl } from '../../config';
+import CropWindow from '../views/components/crop-window.vue';
+import ProgressDialog from '../views/components/progress-dialog.vue';
+
+export default (os: OS) => (cb, file = null) => {
+	const fileSelected = file => {
+
+		const w = new CropWindow({
+			propsData: {
+				file: file,
+				title: 'アバターとして表示する部分を選択',
+				aspectRatio: 1 / 1
+			}
+		}).$mount();
+
+		w.$once('cropped', blob => {
+			const data = new FormData();
+			data.append('i', os.i.token);
+			data.append('file', blob, file.name + '.cropped.png');
+
+			os.api('drive/folders/find', {
+				name: 'アイコン'
+			}).then(iconFolder => {
+				if (iconFolder.length === 0) {
+					os.api('drive/folders/create', {
+						name: 'アイコン'
+					}).then(iconFolder => {
+						upload(data, iconFolder);
+					});
+				} else {
+					upload(data, iconFolder[0]);
+				}
+			});
+		});
+
+		w.$once('skipped', () => {
+			set(file);
+		});
+
+		document.body.appendChild(w.$el);
+	};
+
+	const upload = (data, folder) => {
+		const dialog = new ProgressDialog({
+			propsData: {
+				title: '新しいアバターをアップロードしています'
+			}
+		}).$mount();
+		document.body.appendChild(dialog.$el);
+
+		if (folder) data.append('folder_id', folder.id);
+
+		const xhr = new XMLHttpRequest();
+		xhr.open('POST', apiUrl + '/drive/files/create', true);
+		xhr.onload = e => {
+			const file = JSON.parse((e.target as any).response);
+			(dialog as any).close();
+			set(file);
+		};
+
+		xhr.upload.onprogress = e => {
+			if (e.lengthComputable) (dialog as any).updateProgress(e.loaded, e.total);
+		};
+
+		xhr.send(data);
+	};
+
+	const set = file => {
+		os.api('i/update', {
+			avatar_id: file.id
+		}).then(i => {
+			os.apis.dialog({
+				title: '%fa:info-circle%アバターを更新しました',
+				text: '新しいアバターが反映されるまで時間がかかる場合があります。',
+				actions: [{
+					text: 'わかった'
+				}]
+			});
+
+			if (cb) cb(i);
+		});
+	};
+
+	if (file) {
+		fileSelected(file);
+	} else {
+		os.apis.chooseDriveFile({
+			multiple: false,
+			title: '%fa:image%アバターにする画像を選択'
+		}).then(file => {
+			fileSelected(file);
+		});
+	}
+};
diff --git a/src/web/app/desktop/api/update-banner.ts b/src/web/app/desktop/api/update-banner.ts
new file mode 100644
index 000000000..575161658
--- /dev/null
+++ b/src/web/app/desktop/api/update-banner.ts
@@ -0,0 +1,95 @@
+import OS from '../../common/mios';
+import { apiUrl } from '../../config';
+import CropWindow from '../views/components/crop-window.vue';
+import ProgressDialog from '../views/components/progress-dialog.vue';
+
+export default (os: OS) => (cb, file = null) => {
+	const fileSelected = file => {
+
+		const w = new CropWindow({
+			propsData: {
+				file: file,
+				title: 'バナーとして表示する部分を選択',
+				aspectRatio: 16 / 9
+			}
+		}).$mount();
+
+		w.$once('cropped', blob => {
+			const data = new FormData();
+			data.append('i', os.i.token);
+			data.append('file', blob, file.name + '.cropped.png');
+
+			os.api('drive/folders/find', {
+				name: 'バナー'
+			}).then(bannerFolder => {
+				if (bannerFolder.length === 0) {
+					os.api('drive/folders/create', {
+						name: 'バナー'
+					}).then(iconFolder => {
+						upload(data, iconFolder);
+					});
+				} else {
+					upload(data, bannerFolder[0]);
+				}
+			});
+		});
+
+		w.$once('skipped', () => {
+			set(file);
+		});
+
+		document.body.appendChild(w.$el);
+	};
+
+	const upload = (data, folder) => {
+		const dialog = new ProgressDialog({
+			propsData: {
+				title: '新しいバナーをアップロードしています'
+			}
+		}).$mount();
+		document.body.appendChild(dialog.$el);
+
+		if (folder) data.append('folder_id', folder.id);
+
+		const xhr = new XMLHttpRequest();
+		xhr.open('POST', apiUrl + '/drive/files/create', true);
+		xhr.onload = e => {
+			const file = JSON.parse((e.target as any).response);
+			(dialog as any).close();
+			set(file);
+		};
+
+		xhr.upload.onprogress = e => {
+			if (e.lengthComputable) (dialog as any).updateProgress(e.loaded, e.total);
+		};
+
+		xhr.send(data);
+	};
+
+	const set = file => {
+		os.api('i/update', {
+			avatar_id: file.id
+		}).then(i => {
+			os.apis.dialog({
+				title: '%fa:info-circle%バナーを更新しました',
+				text: '新しいバナーが反映されるまで時間がかかる場合があります。',
+				actions: [{
+					text: 'わかった'
+				}]
+			});
+
+			if (cb) cb(i);
+		});
+	};
+
+	if (file) {
+		fileSelected(file);
+	} else {
+		os.apis.chooseDriveFile({
+			multiple: false,
+			title: '%fa:image%バナーにする画像を選択'
+		}).then(file => {
+			fileSelected(file);
+		});
+	}
+};
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index 2477f62f4..b647f4031 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -6,7 +6,7 @@
 import './style.styl';
 
 import init from '../init';
-import fuckAdBlock from './scripts/fuck-ad-block';
+import fuckAdBlock from '../common/scripts/fuck-ad-block';
 import HomeStreamManager from '../common/scripts/streaming/home-stream-manager';
 import composeNotification from '../common/scripts/compose-notification';
 
@@ -15,6 +15,9 @@ import chooseDriveFile from './api/choose-drive-file';
 import dialog from './api/dialog';
 import input from './api/input';
 import post from './api/post';
+import notify from './api/notify';
+import updateAvatar from './api/update-avatar';
+import updateBanner from './api/update-banner';
 
 import MkIndex from './views/pages/index.vue';
 import MkUser from './views/pages/user/user.vue';
@@ -25,24 +28,27 @@ import MkDrive from './views/pages/drive.vue';
  * init
  */
 init(async (launch) => {
-	/**
-	 * Fuck AD Block
-	 */
-	fuckAdBlock();
-
 	// Register directives
 	require('./views/directives');
 
 	// Register components
 	require('./views/components');
 
-	const app = launch({
+	const [app, os] = launch(os => ({
 		chooseDriveFolder,
 		chooseDriveFile,
 		dialog,
 		input,
-		post
-	});
+		post,
+		notify,
+		updateAvatar: updateAvatar(os),
+		updateBanner: updateBanner(os)
+	}));
+
+	/**
+	 * Fuck AD Block
+	 */
+	fuckAdBlock(os);
 
 	/**
 	 * Init Notification
diff --git a/src/web/app/desktop/scripts/dialog.ts b/src/web/app/desktop/scripts/dialog.ts
deleted file mode 100644
index 816ba4b5f..000000000
--- a/src/web/app/desktop/scripts/dialog.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as riot from 'riot';
-
-export default (title, text, buttons, canThrough?, onThrough?) => {
-	const dialog = document.body.appendChild(document.createElement('mk-dialog'));
-	const controller = riot.observable();
-	(riot as any).mount(dialog, {
-		controller: controller,
-		title: title,
-		text: text,
-		buttons: buttons,
-		canThrough: canThrough,
-		onThrough: onThrough
-	});
-	controller.trigger('open');
-	return controller;
-};
diff --git a/src/web/app/desktop/scripts/fuck-ad-block.ts b/src/web/app/desktop/scripts/fuck-ad-block.ts
deleted file mode 100644
index ddeb600b6..000000000
--- a/src/web/app/desktop/scripts/fuck-ad-block.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-require('fuckadblock');
-import dialog from './dialog';
-
-declare const fuckAdBlock: any;
-
-export default () => {
-	if (fuckAdBlock === undefined) {
-		adBlockDetected();
-	} else {
-		fuckAdBlock.onDetected(adBlockDetected);
-	}
-};
-
-function adBlockDetected() {
-	dialog('%fa:exclamation-triangle%広告ブロッカーを無効にしてください',
-		'<strong>Misskeyは広告を掲載していません</strong>が、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。',
-	[{
-		text: 'OK'
-	}]);
-}
diff --git a/src/web/app/desktop/scripts/input-dialog.ts b/src/web/app/desktop/scripts/input-dialog.ts
deleted file mode 100644
index b06d011c6..000000000
--- a/src/web/app/desktop/scripts/input-dialog.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import * as riot from 'riot';
-
-export default (title, placeholder, defaultValue, onOk, onCancel) => {
-	const dialog = document.body.appendChild(document.createElement('mk-input-dialog'));
-	return (riot as any).mount(dialog, {
-		title: title,
-		placeholder: placeholder,
-		'default': defaultValue,
-		onOk: onOk,
-		onCancel: onCancel
-	});
-};
diff --git a/src/web/app/desktop/scripts/not-implemented-exception.ts b/src/web/app/desktop/scripts/not-implemented-exception.ts
deleted file mode 100644
index b4660fa62..000000000
--- a/src/web/app/desktop/scripts/not-implemented-exception.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import dialog from './dialog';
-
-export default () => {
-	dialog('%fa:exclamation-triangle%Not implemented yet',
-		'要求された操作は実装されていません。<br>→<a href="https://github.com/syuilo/misskey" target="_blank">Misskeyの開発に参加する</a>', [{
-		text: 'OK'
-	}]);
-};
diff --git a/src/web/app/desktop/scripts/notify.ts b/src/web/app/desktop/scripts/notify.ts
deleted file mode 100644
index 2e6cbdeed..000000000
--- a/src/web/app/desktop/scripts/notify.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import * as riot from 'riot';
-
-export default message => {
-	const notification = document.body.appendChild(document.createElement('mk-ui-notification'));
-	(riot as any).mount(notification, {
-		message: message
-	});
-};
diff --git a/src/web/app/desktop/scripts/scroll-follower.ts b/src/web/app/desktop/scripts/scroll-follower.ts
deleted file mode 100644
index 05072958c..000000000
--- a/src/web/app/desktop/scripts/scroll-follower.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * 要素をスクロールに追従させる
- */
-export default class ScrollFollower {
-	private follower: Element;
-	private containerTop: number;
-	private topPadding: number;
-
-	constructor(follower: Element, topPadding: number) {
-		//#region
-		this.follow = this.follow.bind(this);
-		//#endregion
-
-		this.follower = follower;
-		this.containerTop = follower.getBoundingClientRect().top;
-		this.topPadding = topPadding;
-
-		window.addEventListener('scroll', this.follow);
-		window.addEventListener('resize', this.follow);
-	}
-
-	/**
-	 * 追従解除
-	 */
-	public dispose() {
-		window.removeEventListener('scroll', this.follow);
-		window.removeEventListener('resize', this.follow);
-	}
-
-	private follow() {
-		const windowBottom = window.scrollY + window.innerHeight;
-		const windowTop = window.scrollY + this.topPadding;
-
-		const rect = this.follower.getBoundingClientRect();
-		const followerBottom = (rect.top + window.scrollY) + rect.height;
-		const screenHeight = window.innerHeight - this.topPadding;
-
-		// スクロールの上部(+余白)がフォロワーコンテナの上部よりも上方にある
-		if (window.scrollY + this.topPadding < this.containerTop) {
-			// フォロワーをコンテナの最上部に合わせる
-			(this.follower.parentNode as any).style.marginTop = '0px';
-			return;
-		}
-
-		// スクロールの下部がフォロワーの下部よりも下方にある かつ 表示領域の縦幅がフォロワーの縦幅よりも狭い
-		if (windowBottom > followerBottom && rect.height > screenHeight) {
-			// フォロワーの下部をスクロール下部に合わせる
-			const top = (windowBottom - rect.height) - this.containerTop;
-			(this.follower.parentNode as any).style.marginTop = `${top}px`;
-			return;
-		}
-
-		// スクロールの上部(+余白)がフォロワーの上部よりも上方にある または 表示領域の縦幅がフォロワーの縦幅よりも広い
-		if (windowTop < rect.top + window.scrollY || rect.height < screenHeight) {
-			// フォロワーの上部をスクロール上部(+余白)に合わせる
-			const top = windowTop - this.containerTop;
-			(this.follower.parentNode as any).style.marginTop = `${top}px`;
-			return;
-		}
-	}
-}
diff --git a/src/web/app/desktop/scripts/update-avatar.ts b/src/web/app/desktop/scripts/update-avatar.ts
deleted file mode 100644
index fea5db80b..000000000
--- a/src/web/app/desktop/scripts/update-avatar.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-declare const _API_URL_: string;
-
-import * as riot from 'riot';
-import dialog from './dialog';
-import api from '../../common/scripts/api';
-
-export default (I, cb, file = null) => {
-	const fileSelected = file => {
-		const cropper = (riot as any).mount(document.body.appendChild(document.createElement('mk-crop-window')), {
-			file: file,
-			title: 'アバターとして表示する部分を選択',
-			aspectRatio: 1 / 1
-		})[0];
-
-		cropper.on('cropped', blob => {
-			const data = new FormData();
-			data.append('i', I.token);
-			data.append('file', blob, file.name + '.cropped.png');
-
-			api(I, 'drive/folders/find', {
-				name: 'アイコン'
-			}).then(iconFolder => {
-				if (iconFolder.length === 0) {
-					api(I, 'drive/folders/create', {
-						name: 'アイコン'
-					}).then(iconFolder => {
-						upload(data, iconFolder);
-					});
-				} else {
-					upload(data, iconFolder[0]);
-				}
-			});
-		});
-
-		cropper.on('skipped', () => {
-			set(file);
-		});
-	};
-
-	const upload = (data, folder) => {
-		const progress = (riot as any).mount(document.body.appendChild(document.createElement('mk-progress-dialog')), {
-			title: '新しいアバターをアップロードしています'
-		})[0];
-
-		if (folder) data.append('folder_id', folder.id);
-
-		const xhr = new XMLHttpRequest();
-		xhr.open('POST', _API_URL_ + '/drive/files/create', true);
-		xhr.onload = e => {
-			const file = JSON.parse((e.target as any).response);
-			progress.close();
-			set(file);
-		};
-
-		xhr.upload.onprogress = e => {
-			if (e.lengthComputable) progress.updateProgress(e.loaded, e.total);
-		};
-
-		xhr.send(data);
-	};
-
-	const set = file => {
-		api(I, 'i/update', {
-			avatar_id: file.id
-		}).then(i => {
-			dialog('%fa:info-circle%アバターを更新しました',
-				'新しいアバターが反映されるまで時間がかかる場合があります。',
-			[{
-				text: 'わかった'
-			}]);
-
-			if (cb) cb(i);
-		});
-	};
-
-	if (file) {
-		fileSelected(file);
-	} else {
-		const browser = (riot as any).mount(document.body.appendChild(document.createElement('mk-select-file-from-drive-window')), {
-			multiple: false,
-			title: '%fa:image%アバターにする画像を選択'
-		})[0];
-
-		browser.one('selected', file => {
-			fileSelected(file);
-		});
-	}
-};
diff --git a/src/web/app/desktop/scripts/update-banner.ts b/src/web/app/desktop/scripts/update-banner.ts
deleted file mode 100644
index 325775622..000000000
--- a/src/web/app/desktop/scripts/update-banner.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-declare const _API_URL_: string;
-
-import * as riot from 'riot';
-import dialog from './dialog';
-import api from '../../common/scripts/api';
-
-export default (I, cb, file = null) => {
-	const fileSelected = file => {
-		const cropper = (riot as any).mount(document.body.appendChild(document.createElement('mk-crop-window')), {
-			file: file,
-			title: 'バナーとして表示する部分を選択',
-			aspectRatio: 16 / 9
-		})[0];
-
-		cropper.on('cropped', blob => {
-			const data = new FormData();
-			data.append('i', I.token);
-			data.append('file', blob, file.name + '.cropped.png');
-
-			api(I, 'drive/folders/find', {
-				name: 'バナー'
-			}).then(iconFolder => {
-				if (iconFolder.length === 0) {
-					api(I, 'drive/folders/create', {
-						name: 'バナー'
-					}).then(iconFolder => {
-						upload(data, iconFolder);
-					});
-				} else {
-					upload(data, iconFolder[0]);
-				}
-			});
-		});
-
-		cropper.on('skipped', () => {
-			set(file);
-		});
-	};
-
-	const upload = (data, folder) => {
-		const progress = (riot as any).mount(document.body.appendChild(document.createElement('mk-progress-dialog')), {
-			title: '新しいバナーをアップロードしています'
-		})[0];
-
-		if (folder) data.append('folder_id', folder.id);
-
-		const xhr = new XMLHttpRequest();
-		xhr.open('POST', _API_URL_ + '/drive/files/create', true);
-		xhr.onload = e => {
-			const file = JSON.parse((e.target as any).response);
-			progress.close();
-			set(file);
-		};
-
-		xhr.upload.onprogress = e => {
-			if (e.lengthComputable) progress.updateProgress(e.loaded, e.total);
-		};
-
-		xhr.send(data);
-	};
-
-	const set = file => {
-		api(I, 'i/update', {
-			banner_id: file.id
-		}).then(i => {
-			dialog('%fa:info-circle%バナーを更新しました',
-				'新しいバナーが反映されるまで時間がかかる場合があります。',
-			[{
-				text: 'わかりました。'
-			}]);
-
-			if (cb) cb(i);
-		});
-	};
-
-	if (file) {
-		fileSelected(file);
-	} else {
-		const browser = (riot as any).mount(document.body.appendChild(document.createElement('mk-select-file-from-drive-window')), {
-			multiple: false,
-			title: '%fa:image%バナーにする画像を選択'
-		})[0];
-
-		browser.one('selected', file => {
-			fileSelected(file);
-		});
-	}
-};
diff --git a/src/web/app/desktop/views/components/dialog.vue b/src/web/app/desktop/views/components/dialog.vue
index e92050dba..f089b19a4 100644
--- a/src/web/app/desktop/views/components/dialog.vue
+++ b/src/web/app/desktop/views/components/dialog.vue
@@ -5,7 +5,7 @@
 		<header v-html="title"></header>
 		<div class="body" v-html="text"></div>
 		<div class="buttons">
-			<button v-for="button in buttons" @click="click(button)" :key="button.id">{{ button.text }}</button>
+			<button v-for="button in buttons" @click="click(button)">{{ button.text }}</button>
 		</div>
 	</div>
 </div>
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index f117f8cc5..c362d500e 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -40,7 +40,6 @@ import Vue from 'vue';
 import * as Sortable from 'sortablejs';
 import Autocomplete from '../../scripts/autocomplete';
 import getKao from '../../../common/scripts/get-kao';
-import notify from '../../scripts/notify';
 
 export default Vue.extend({
 	props: ['reply', 'repost'],
@@ -200,13 +199,13 @@ export default Vue.extend({
 				this.clear();
 				this.deleteDraft();
 				this.$emit('posted');
-				notify(this.repost
+				(this as any).apis.notify(this.repost
 					? '%i18n:desktop.tags.mk-post-form.reposted%'
 					: this.reply
 						? '%i18n:desktop.tags.mk-post-form.replied%'
 						: '%i18n:desktop.tags.mk-post-form.posted%');
 			}).catch(err => {
-				notify(this.repost
+				(this as any).apis.notify(this.repost
 					? '%i18n:desktop.tags.mk-post-form.repost-failed%'
 					: this.reply
 						? '%i18n:desktop.tags.mk-post-form.reply-failed%'
diff --git a/src/web/app/desktop/views/components/repost-form.vue b/src/web/app/desktop/views/components/repost-form.vue
index d4a6186c4..5bf7eaaf0 100644
--- a/src/web/app/desktop/views/components/repost-form.vue
+++ b/src/web/app/desktop/views/components/repost-form.vue
@@ -16,7 +16,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import notify from '../../scripts/notify';
 
 export default Vue.extend({
 	props: ['post'],
@@ -33,9 +32,9 @@ export default Vue.extend({
 				repost_id: this.post.id
 			}).then(data => {
 				this.$emit('posted');
-				notify('%i18n:desktop.tags.mk-repost-form.success%');
+				(this as any).apis.notify('%i18n:desktop.tags.mk-repost-form.success%');
 			}).catch(err => {
-				notify('%i18n:desktop.tags.mk-repost-form.failure%');
+				(this as any).apis.notify('%i18n:desktop.tags.mk-repost-form.failure%');
 			}).then(() => {
 				this.wait = false;
 			});
diff --git a/src/web/app/desktop/views/components/settings.profile.vue b/src/web/app/desktop/views/components/settings.profile.vue
index c8834ca25..dcc031c27 100644
--- a/src/web/app/desktop/views/components/settings.profile.vue
+++ b/src/web/app/desktop/views/components/settings.profile.vue
@@ -27,7 +27,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import notify from '../../scripts/notify';
 
 export default Vue.extend({
 	data() {
@@ -59,7 +58,7 @@ export default Vue.extend({
 				description: this.description || null,
 				birthday: this.birthday || null
 			}).then(() => {
-				notify('プロフィールを更新しました');
+				(this as any).apis.notify('プロフィールを更新しました');
 			});
 		}
 	}
diff --git a/src/web/app/desktop/views/components/ui-notification.vue b/src/web/app/desktop/views/components/ui-notification.vue
index 6f7b46cb7..9983f02c5 100644
--- a/src/web/app/desktop/views/components/ui-notification.vue
+++ b/src/web/app/desktop/views/components/ui-notification.vue
@@ -11,24 +11,26 @@ import * as anime from 'animejs';
 export default Vue.extend({
 	props: ['message'],
 	mounted() {
-		anime({
-			targets: this.$el,
-			opacity: 1,
-			translateY: [-64, 0],
-			easing: 'easeOutElastic',
-			duration: 500
-		});
-
-		setTimeout(() => {
+		this.$nextTick(() => {
 			anime({
 				targets: this.$el,
-				opacity: 0,
-				translateY: -64,
-				duration: 500,
-				easing: 'easeInElastic',
-				complete: () => this.$destroy()
+				opacity: 1,
+				translateY: [-64, 0],
+				easing: 'easeOutElastic',
+				duration: 500
 			});
-		}, 6000);
+
+			setTimeout(() => {
+				anime({
+					targets: this.$el,
+					opacity: 0,
+					translateY: -64,
+					duration: 500,
+					easing: 'easeInElastic',
+					complete: () => this.$destroy()
+				});
+			}, 6000);
+		});
 	}
 });
 </script>
diff --git a/src/web/app/desktop/views/pages/user/user.header.vue b/src/web/app/desktop/views/pages/user/user.header.vue
index 81174f657..67d110f2f 100644
--- a/src/web/app/desktop/views/pages/user/user.header.vue
+++ b/src/web/app/desktop/views/pages/user/user.header.vue
@@ -22,7 +22,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import updateBanner from '../../../scripts/update-banner';
 
 export default Vue.extend({
 	props: ['user'],
@@ -53,7 +52,7 @@ export default Vue.extend({
 		onBannerClick() {
 			if (!(this as any).os.isSignedIn || (this as any).os.i.id != this.user.id) return;
 
-			updateBanner((this as any).os.i, i => {
+			(this as any).apis.updateBanner((this as any).os.i, i => {
 				this.user.banner_url = i.banner_url;
 			});
 		}
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 9e49c4f0f..b814a1806 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -34,7 +34,7 @@ Vue.mixin({
 import App from './app.vue';
 
 import checkForUpdate from './common/scripts/check-for-update';
-import MiOS from './common/mios';
+import MiOS, { API } from './common/mios';
 
 /**
  * APP ENTRY POINT!
@@ -79,59 +79,32 @@ if (localStorage.getItem('should-refresh') == 'true') {
 	location.reload(true);
 }
 
-type API = {
-	chooseDriveFile: (opts: {
-		title?: string;
-		currentFolder?: any;
-		multiple?: boolean;
-	}) => Promise<any>;
-
-	chooseDriveFolder: (opts: {
-		title?: string;
-		currentFolder?: any;
-	}) => Promise<any>;
-
-	dialog: (opts: {
-		title: string;
-		text: string;
-		actions: Array<{
-			text: string;
-			id: string;
-		}>;
-	}) => Promise<string>;
-
-	input: (opts: {
-		title: string;
-		placeholder?: string;
-		default?: string;
-	}) => Promise<string>;
-
-	post: () => void;
-};
-
 // MiOSを初期化してコールバックする
-export default (callback: (launch: (api: API) => Vue) => void, sw = false) => {
+export default (callback: (launch: (api: (os: MiOS) => API) => [Vue, MiOS]) => void, sw = false) => {
 	const os = new MiOS(sw);
 
 	os.init(() => {
 		// アプリ基底要素マウント
 		document.body.innerHTML = '<div id="app"></div>';
 
-		const launch = (api: API) => {
+		const launch = (api: (os: MiOS) => API) => {
+			os.apis = api(os);
 			Vue.mixin({
 				created() {
 					(this as any).os = os;
 					(this as any).api = os.api;
-					(this as any).apis = api;
+					(this as any).apis = os.apis;
 				}
 			});
 
-			return new Vue({
+			const app = new Vue({
 				router: new VueRouter({
 					mode: 'history'
 				}),
 				render: createEl => createEl(App)
 			}).$mount('#app');
+
+			return [app, os] as [Vue, MiOS];
 		};
 
 		try {

From 19b558c85267e5b47b15b2869ae817015e50f030 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 02:55:22 +0900
Subject: [PATCH 233/286] wip

---
 src/web/app/common/scripts/is-promise.ts | 1 -
 1 file changed, 1 deletion(-)
 delete mode 100644 src/web/app/common/scripts/is-promise.ts

diff --git a/src/web/app/common/scripts/is-promise.ts b/src/web/app/common/scripts/is-promise.ts
deleted file mode 100644
index 3b4cd70b4..000000000
--- a/src/web/app/common/scripts/is-promise.ts
+++ /dev/null
@@ -1 +0,0 @@
-export default x => typeof x.then == 'function';

From 9c0521316deaad6c37b43ac776ef50f976786224 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 04:52:52 +0900
Subject: [PATCH 234/286] wip

---
 package.json                                  |   1 +
 src/web/app/desktop/-tags/crop-window.tag     | 196 ------------------
 .../desktop/views/components/crop-window.vue  | 169 +++++++++++++++
 .../desktop/views/pages/user/user.header.vue  |   6 +-
 4 files changed, 173 insertions(+), 199 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/crop-window.tag
 create mode 100644 src/web/app/desktop/views/components/crop-window.vue

diff --git a/package.json b/package.json
index 727c4af71..033f76c30 100644
--- a/package.json
+++ b/package.json
@@ -182,6 +182,7 @@
 		"uuid": "3.2.1",
 		"vhost": "3.0.2",
 		"vue": "^2.5.13",
+		"vue-cropperjs": "^2.2.0",
 		"vue-js-modal": "^1.3.9",
 		"vue-loader": "^14.1.1",
 		"vue-router": "^3.0.1",
diff --git a/src/web/app/desktop/-tags/crop-window.tag b/src/web/app/desktop/-tags/crop-window.tag
deleted file mode 100644
index c26f74b12..000000000
--- a/src/web/app/desktop/-tags/crop-window.tag
+++ /dev/null
@@ -1,196 +0,0 @@
-<mk-crop-window>
-	<mk-window ref="window" is-modal={ true } width={ '800px' }>
-		<yield to="header">%fa:crop%{ parent.title }</yield>
-		<yield to="content">
-			<div class="body"><img ref="img" src={ parent.image.url + '?thumbnail&quality=80' } alt=""/></div>
-			<div class="action">
-				<button class="skip" @click="parent.skip">クロップをスキップ</button>
-				<button class="cancel" @click="parent.cancel">キャンセル</button>
-				<button class="ok" @click="parent.ok">決定</button>
-			</div>
-		</yield>
-	</mk-window>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> mk-window
-				[data-yield='header']
-					> [data-fa]
-						margin-right 4px
-
-				[data-yield='content']
-
-					> .body
-						> img
-							width 100%
-							max-height 400px
-
-					.cropper-modal {
-						opacity: 0.8;
-					}
-
-					.cropper-view-box {
-						outline-color: $theme-color;
-					}
-
-					.cropper-line, .cropper-point {
-						background-color: $theme-color;
-					}
-
-					.cropper-bg {
-						animation: cropper-bg 0.5s linear infinite;
-					}
-
-					@-webkit-keyframes cropper-bg {
-						0% {
-							background-position: 0 0;
-						}
-
-						100% {
-							background-position: -8px -8px;
-						}
-					}
-
-					@-moz-keyframes cropper-bg {
-						0% {
-							background-position: 0 0;
-						}
-
-						100% {
-							background-position: -8px -8px;
-						}
-					}
-
-					@-ms-keyframes cropper-bg {
-						0% {
-							background-position: 0 0;
-						}
-
-						100% {
-							background-position: -8px -8px;
-						}
-					}
-
-					@keyframes cropper-bg {
-						0% {
-							background-position: 0 0;
-						}
-
-						100% {
-							background-position: -8px -8px;
-						}
-					}
-
-					> .action
-						height 72px
-						background lighten($theme-color, 95%)
-
-						.ok
-						.cancel
-						.skip
-							display block
-							position absolute
-							bottom 16px
-							cursor pointer
-							padding 0
-							margin 0
-							height 40px
-							font-size 1em
-							outline none
-							border-radius 4px
-
-							&:focus
-								&:after
-									content ""
-									pointer-events none
-									position absolute
-									top -5px
-									right -5px
-									bottom -5px
-									left -5px
-									border 2px solid rgba($theme-color, 0.3)
-									border-radius 8px
-
-							&:disabled
-								opacity 0.7
-								cursor default
-
-						.ok
-						.cancel
-							width 120px
-
-						.ok
-							right 16px
-							color $theme-color-foreground
-							background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
-							border solid 1px lighten($theme-color, 15%)
-
-							&:not(:disabled)
-								font-weight bold
-
-							&:hover:not(:disabled)
-								background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
-								border-color $theme-color
-
-							&:active:not(:disabled)
-								background $theme-color
-								border-color $theme-color
-
-						.cancel
-						.skip
-							color #888
-							background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
-							border solid 1px #e2e2e2
-
-							&:hover
-								background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
-								border-color #dcdcdc
-
-							&:active
-								background #ececec
-								border-color #dcdcdc
-
-						.cancel
-							right 148px
-
-						.skip
-							left 16px
-							width 150px
-
-	</style>
-	<script lang="typescript">
-		const Cropper = require('cropperjs');
-
-		this.image = this.opts.file;
-		this.title = this.opts.title;
-		this.aspectRatio = this.opts.aspectRatio;
-		this.cropper = null;
-
-		this.on('mount', () => {
-			this.img = this.$refs.window.refs.img;
-			this.cropper = new Cropper(this.img, {
-				aspectRatio: this.aspectRatio,
-				highlight: false,
-				viewMode: 1
-			});
-		});
-
-		this.ok = () => {
-			this.cropper.getCroppedCanvas().toBlob(blob => {
-				this.$emit('cropped', blob);
-				this.$refs.window.close();
-			});
-		};
-
-		this.skip = () => {
-			this.$emit('skipped');
-			this.$refs.window.close();
-		};
-
-		this.cancel = () => {
-			this.$emit('canceled');
-			this.$refs.window.close();
-		};
-	</script>
-</mk-crop-window>
diff --git a/src/web/app/desktop/views/components/crop-window.vue b/src/web/app/desktop/views/components/crop-window.vue
new file mode 100644
index 000000000..2ba62a3a6
--- /dev/null
+++ b/src/web/app/desktop/views/components/crop-window.vue
@@ -0,0 +1,169 @@
+<template>
+	<mk-window ref="window" is-modal width="800px">
+		<span slot="header">%fa:crop%{{ title }}</span>
+		<div class="body">
+			<vue-cropper
+				:src="image.url"
+				:view-mode="1"
+			/>
+		</div>
+		<div :class="$style.actions">
+			<button :class="$style.skip" @click="skip">クロップをスキップ</button>
+			<button :class="$style.cancel" @click="cancel">キャンセル</button>
+			<button :class="$style.ok" @click="ok">決定</button>
+		</div>
+	</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		image: {
+			type: Object,
+			required: true
+		},
+		title: {
+			type: String,
+			required: true
+		},
+		aspectRatio: {
+			type: Number,
+			required: true
+		}
+	},
+	methods: {
+		ok() {
+			(this.$refs.cropper as any).getCroppedCanvas().toBlob(blob => {
+				this.$emit('cropped', blob);
+				(this.$refs.window as any).close();
+			});
+		},
+
+		skip() {
+			this.$emit('skipped');
+			(this.$refs.window as any).close();
+		},
+
+		cancel() {
+			this.$emit('canceled');
+			(this.$refs.window as any).close();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.header
+	> [data-fa]
+		margin-right 4px
+
+.img
+	width 100%
+	max-height 400px
+
+.actions
+	height 72px
+	background lighten($theme-color, 95%)
+
+.ok
+.cancel
+.skip
+	display block
+	position absolute
+	bottom 16px
+	cursor pointer
+	padding 0
+	margin 0
+	height 40px
+	font-size 1em
+	outline none
+	border-radius 4px
+
+	&:focus
+		&:after
+			content ""
+			pointer-events none
+			position absolute
+			top -5px
+			right -5px
+			bottom -5px
+			left -5px
+			border 2px solid rgba($theme-color, 0.3)
+			border-radius 8px
+
+	&:disabled
+		opacity 0.7
+		cursor default
+
+.ok
+.cancel
+	width 120px
+
+.ok
+	right 16px
+	color $theme-color-foreground
+	background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+	border solid 1px lighten($theme-color, 15%)
+
+	&:not(:disabled)
+		font-weight bold
+
+	&:hover:not(:disabled)
+		background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
+		border-color $theme-color
+
+	&:active:not(:disabled)
+		background $theme-color
+		border-color $theme-color
+
+.cancel
+.skip
+	color #888
+	background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
+	border solid 1px #e2e2e2
+
+	&:hover
+		background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
+		border-color #dcdcdc
+
+	&:active
+		background #ececec
+		border-color #dcdcdc
+
+.cancel
+	right 148px
+
+.skip
+	left 16px
+	width 150px
+
+</style>
+
+<style lang="stylus">
+.cropper-modal {
+	opacity: 0.8;
+}
+
+.cropper-view-box {
+	outline-color: $theme-color;
+}
+
+.cropper-line, .cropper-point {
+	background-color: $theme-color;
+}
+
+.cropper-bg {
+	animation: cropper-bg 0.5s linear infinite;
+}
+
+@keyframes cropper-bg {
+	0% {
+		background-position: 0 0;
+	}
+
+	100% {
+		background-position: -8px -8px;
+	}
+}
+</style>
diff --git a/src/web/app/desktop/views/pages/user/user.header.vue b/src/web/app/desktop/views/pages/user/user.header.vue
index 67d110f2f..6c8375f16 100644
--- a/src/web/app/desktop/views/pages/user/user.header.vue
+++ b/src/web/app/desktop/views/pages/user/user.header.vue
@@ -12,9 +12,9 @@
 			<p class="location" v-if="user.profile.location">%fa:map-marker%{{ user.profile.location }}</p>
 		</div>
 		<footer>
-			<a :href="`/${user.username}`" :data-active="$parent.page == 'home'">%fa:home%概要</a>
-			<a :href="`/${user.username}/media`" :data-active="$parent.page == 'media'">%fa:image%メディア</a>
-			<a :href="`/${user.username}/graphs`" :data-active="$parent.page == 'graphs'">%fa:chart-bar%グラフ</a>
+			<router-link :to="`/${user.username}`" :data-active="$parent.page == 'home'">%fa:home%概要</router-link>
+			<router-link :to="`/${user.username}/media`" :data-active="$parent.page == 'media'">%fa:image%メディア</router-link>
+			<router-link :to="`/${user.username}/graphs`" :data-active="$parent.page == 'graphs'">%fa:chart-bar%グラフ</router-link>
 		</footer>
 	</div>
 </div>

From 6573f36485530a37f6bdd5786477b3ad904a9cbf Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 05:55:19 +0900
Subject: [PATCH 235/286] wip

---
 src/web/app/desktop/api/update-avatar.ts      |  7 ++++--
 src/web/app/desktop/api/update-banner.ts      |  9 ++++---
 .../desktop/views/components/crop-window.vue  | 11 ++++++--
 .../app/desktop/views/components/drive.vue    |  2 +-
 .../views/components/progress-dialog.vue      | 25 ++++++++++---------
 .../views/components/settings.profile.vue     |  6 +----
 src/web/app/init.ts                           | 10 +++++---
 webpack/webpack.config.ts                     |  6 +++++
 8 files changed, 47 insertions(+), 29 deletions(-)

diff --git a/src/web/app/desktop/api/update-avatar.ts b/src/web/app/desktop/api/update-avatar.ts
index eff072834..c3e0ce14c 100644
--- a/src/web/app/desktop/api/update-avatar.ts
+++ b/src/web/app/desktop/api/update-avatar.ts
@@ -8,7 +8,7 @@ export default (os: OS) => (cb, file = null) => {
 
 		const w = new CropWindow({
 			propsData: {
-				file: file,
+				image: file,
 				title: 'アバターとして表示する部分を選択',
 				aspectRatio: 1 / 1
 			}
@@ -60,7 +60,7 @@ export default (os: OS) => (cb, file = null) => {
 		};
 
 		xhr.upload.onprogress = e => {
-			if (e.lengthComputable) (dialog as any).updateProgress(e.loaded, e.total);
+			if (e.lengthComputable) (dialog as any).update(e.loaded, e.total);
 		};
 
 		xhr.send(data);
@@ -70,6 +70,9 @@ export default (os: OS) => (cb, file = null) => {
 		os.api('i/update', {
 			avatar_id: file.id
 		}).then(i => {
+			os.i.avatar_id = i.avatar_id;
+			os.i.avatar_url = i.avatar_url;
+
 			os.apis.dialog({
 				title: '%fa:info-circle%アバターを更新しました',
 				text: '新しいアバターが反映されるまで時間がかかる場合があります。',
diff --git a/src/web/app/desktop/api/update-banner.ts b/src/web/app/desktop/api/update-banner.ts
index 575161658..9e94dc423 100644
--- a/src/web/app/desktop/api/update-banner.ts
+++ b/src/web/app/desktop/api/update-banner.ts
@@ -8,7 +8,7 @@ export default (os: OS) => (cb, file = null) => {
 
 		const w = new CropWindow({
 			propsData: {
-				file: file,
+				image: file,
 				title: 'バナーとして表示する部分を選択',
 				aspectRatio: 16 / 9
 			}
@@ -60,7 +60,7 @@ export default (os: OS) => (cb, file = null) => {
 		};
 
 		xhr.upload.onprogress = e => {
-			if (e.lengthComputable) (dialog as any).updateProgress(e.loaded, e.total);
+			if (e.lengthComputable) (dialog as any).update(e.loaded, e.total);
 		};
 
 		xhr.send(data);
@@ -68,8 +68,11 @@ export default (os: OS) => (cb, file = null) => {
 
 	const set = file => {
 		os.api('i/update', {
-			avatar_id: file.id
+			banner_id: file.id
 		}).then(i => {
+			os.i.banner_id = i.banner_id;
+			os.i.banner_url = i.banner_url;
+
 			os.apis.dialog({
 				title: '%fa:info-circle%バナーを更新しました',
 				text: '新しいバナーが反映されるまで時間がかかる場合があります。',
diff --git a/src/web/app/desktop/views/components/crop-window.vue b/src/web/app/desktop/views/components/crop-window.vue
index 2ba62a3a6..27d89a9ff 100644
--- a/src/web/app/desktop/views/components/crop-window.vue
+++ b/src/web/app/desktop/views/components/crop-window.vue
@@ -1,10 +1,12 @@
 <template>
-	<mk-window ref="window" is-modal width="800px">
+	<mk-window ref="window" is-modal width="800px" :can-close="false">
 		<span slot="header">%fa:crop%{{ title }}</span>
 		<div class="body">
-			<vue-cropper
+			<vue-cropper ref="cropper"
 				:src="image.url"
 				:view-mode="1"
+				:aspect-ratio="aspectRatio"
+				:container-style="{ width: '100%', 'max-height': '400px' }"
 			/>
 		</div>
 		<div :class="$style.actions">
@@ -17,7 +19,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import VueCropper from 'vue-cropperjs';
+
 export default Vue.extend({
+	components: {
+		VueCropper
+	},
 	props: {
 		image: {
 			type: Object,
diff --git a/src/web/app/desktop/views/components/drive.vue b/src/web/app/desktop/views/components/drive.vue
index 064e4de66..aed31f2a8 100644
--- a/src/web/app/desktop/views/components/drive.vue
+++ b/src/web/app/desktop/views/components/drive.vue
@@ -49,7 +49,7 @@
 		</div>
 	</div>
 	<div class="dropzone" v-if="draghover"></div>
-	<mk-uploader @change="onChangeUploaderUploads" @uploaded="onUploaderUploaded"/>
+	<mk-uploader ref="uploader" @change="onChangeUploaderUploads" @uploaded="onUploaderUploaded"/>
 	<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/>
 </div>
 </template>
diff --git a/src/web/app/desktop/views/components/progress-dialog.vue b/src/web/app/desktop/views/components/progress-dialog.vue
index 9a925d5b1..ed49b19d7 100644
--- a/src/web/app/desktop/views/components/progress-dialog.vue
+++ b/src/web/app/desktop/views/components/progress-dialog.vue
@@ -1,17 +1,15 @@
 <template>
 <mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="$destroy">
-	<span to="header">{{ title }}<mk-ellipsis/></span>
-	<div to="content">
-		<div :class="$style.body">
-			<p :class="$style.init" v-if="isNaN(value)">待機中<mk-ellipsis/></p>
-			<p :class="$style.percentage" v-if="!isNaN(value)">{{ Math.floor((value / max) * 100) }}</p>
-			<progress :class="$style.progress"
-				v-if="!isNaN(value) && value < max"
-				:value="isNaN(value) ? 0 : value"
-				:max="max"
-			></progress>
-			<div :class="[$style.progress, $style.waiting]" v-if="value >= max"></div>
-		</div>
+	<span slot="header">{{ title }}<mk-ellipsis/></span>
+	<div :class="$style.body">
+		<p :class="$style.init" v-if="isNaN(value)">待機中<mk-ellipsis/></p>
+		<p :class="$style.percentage" v-if="!isNaN(value)">{{ Math.floor((value / max) * 100) }}</p>
+		<progress :class="$style.progress"
+			v-if="!isNaN(value) && value < max"
+			:value="isNaN(value) ? 0 : value"
+			:max="max"
+		></progress>
+		<div :class="[$style.progress, $style.waiting]" v-if="value >= max"></div>
 	</div>
 </mk-window>
 </template>
@@ -30,6 +28,9 @@ export default Vue.extend({
 		update(value, max) {
 			this.value = parseInt(value, 10);
 			this.max = parseInt(max, 10);
+		},
+		close() {
+			(this.$refs.window as any).close();
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/settings.profile.vue b/src/web/app/desktop/views/components/settings.profile.vue
index dcc031c27..97a382d79 100644
--- a/src/web/app/desktop/views/components/settings.profile.vue
+++ b/src/web/app/desktop/views/components/settings.profile.vue
@@ -45,11 +45,7 @@ export default Vue.extend({
 	},
 	methods: {
 		updateAvatar() {
-			(this as any).apis.chooseDriveFile({
-				multiple: false
-			}).then(file => {
-				(this as any).apis.updateAvatar(file);
-			});
+			(this as any).apis.updateAvatar();
 		},
 		save() {
 			(this as any).api('i/update', {
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index b814a1806..02c125efe 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -90,10 +90,12 @@ export default (callback: (launch: (api: (os: MiOS) => API) => [Vue, MiOS]) => v
 		const launch = (api: (os: MiOS) => API) => {
 			os.apis = api(os);
 			Vue.mixin({
-				created() {
-					(this as any).os = os;
-					(this as any).api = os.api;
-					(this as any).apis = os.apis;
+				data() {
+					return {
+						os,
+						api: os.api,
+						apis: os.apis
+					};
 				}
 			});
 
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index fae75059a..3686d0b65 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -100,6 +100,12 @@ module.exports = Object.keys(langs).map(lang => {
 					{ loader: 'css-loader' },
 					{ loader: 'stylus-loader' }
 				]
+			}, {
+				test: /\.css$/,
+				use: [
+					{ loader: 'style-loader' },
+					{ loader: 'css-loader' }
+				]
 			}, {
 				test: /\.ts$/,
 				exclude: /node_modules/,

From 1eecc1fa3d273862b38cf1b51471ac559591597c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 12:27:01 +0900
Subject: [PATCH 236/286] wip

---
 src/web/app/common/-tags/twitter-setting.tag  | 62 ------------------
 .../views/components/twitter-setting.vue      | 64 +++++++++++++++++++
 2 files changed, 64 insertions(+), 62 deletions(-)
 delete mode 100644 src/web/app/common/-tags/twitter-setting.tag
 create mode 100644 src/web/app/common/views/components/twitter-setting.vue

diff --git a/src/web/app/common/-tags/twitter-setting.tag b/src/web/app/common/-tags/twitter-setting.tag
deleted file mode 100644
index a62329083..000000000
--- a/src/web/app/common/-tags/twitter-setting.tag
+++ /dev/null
@@ -1,62 +0,0 @@
-<mk-twitter-setting>
-	<p>%i18n:common.tags.mk-twitter-setting.description%<a href={ _DOCS_URL_ + '/link-to-twitter' } target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p>
-	<p class="account" v-if="I.twitter" title={ 'Twitter ID: ' + I.twitter.user_id }>%i18n:common.tags.mk-twitter-setting.connected-to%: <a href={ 'https://twitter.com/' + I.twitter.screen_name } target="_blank">@{ I.twitter.screen_name }</a></p>
-	<p>
-		<a href={ _API_URL_ + '/connect/twitter' } target="_blank" @click="connect">{ I.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }</a>
-		<span v-if="I.twitter"> or </span>
-		<a href={ _API_URL_ + '/disconnect/twitter' } target="_blank" v-if="I.twitter" @click="disconnect">%i18n:common.tags.mk-twitter-setting.disconnect%</a>
-	</p>
-	<p class="id" v-if="I.twitter">Twitter ID: { I.twitter.user_id }</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			color #4a535a
-
-			.account
-				border solid 1px #e1e8ed
-				border-radius 4px
-				padding 16px
-
-				a
-					font-weight bold
-					color inherit
-
-			.id
-				color #8899a6
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-
-		this.form = null;
-
-		this.on('mount', () => {
-			this.$root.$data.os.i.on('updated', this.onMeUpdated);
-		});
-
-		this.on('unmount', () => {
-			this.$root.$data.os.i.off('updated', this.onMeUpdated);
-		});
-
-		this.onMeUpdated = () => {
-			if (this.$root.$data.os.i.twitter) {
-				if (this.form) this.form.close();
-			}
-		};
-
-		this.connect = e => {
-			e.preventDefault();
-			this.form = window.open(_API_URL_ + '/connect/twitter',
-				'twitter_connect_window',
-				'height=570,width=520');
-			return false;
-		};
-
-		this.disconnect = e => {
-			e.preventDefault();
-			window.open(_API_URL_ + '/disconnect/twitter',
-				'twitter_disconnect_window',
-				'height=570,width=520');
-			return false;
-		};
-	</script>
-</mk-twitter-setting>
diff --git a/src/web/app/common/views/components/twitter-setting.vue b/src/web/app/common/views/components/twitter-setting.vue
new file mode 100644
index 000000000..996f34fb7
--- /dev/null
+++ b/src/web/app/common/views/components/twitter-setting.vue
@@ -0,0 +1,64 @@
+<template>
+<div class="mk-twitter-setting">
+	<p>%i18n:common.tags.mk-twitter-setting.description%<a :href="`${docsUrl}/link-to-twitter`" target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p>
+	<p class="account" v-if="os.i.twitter" :title="`Twitter ID: ${os.i.twitter.user_id}`">%i18n:common.tags.mk-twitter-setting.connected-to%: <a :href="`https://twitter.com/${os.i.twitter.screen_name}`" target="_blank">@{{ I.twitter.screen_name }}</a></p>
+	<p>
+		<a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }}</a>
+		<span v-if="os.i.twitter"> or </span>
+		<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.i.twitter" @click.prevent="disconnect">%i18n:common.tags.mk-twitter-setting.disconnect%</a>
+	</p>
+	<p class="id" v-if="os.i.twitter">Twitter ID: {{ os.i.twitter.user_id }}</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { apiUrl, docsUrl } from '../../../config';
+
+export default Vue.extend({
+	data() {
+		return {
+			form: null,
+			apiUrl,
+			docsUrl
+		};
+	},
+	watch: {
+		'os.i'() {
+			if ((this as any).os.i.twitter) {
+				if (this.form) this.form.close();
+			}
+		}
+	},
+	methods: {
+		connect() {
+			this.form = window.open(apiUrl + '/connect/twitter',
+				'twitter_connect_window',
+				'height=570, width=520');
+		},
+
+		disconnect() {
+			window.open(apiUrl + '/disconnect/twitter',
+				'twitter_disconnect_window',
+				'height=570, width=520');
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-twitter-setting
+	color #4a535a
+
+	.account
+		border solid 1px #e1e8ed
+		border-radius 4px
+		padding 16px
+
+		a
+			font-weight bold
+			color inherit
+
+	.id
+		color #8899a6
+</style>

From de6d77d0cbd2905e021a075b667b6688cd5e06f6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 15:30:03 +0900
Subject: [PATCH 237/286] wip

---
 src/web/app/common/define-widget.ts           |  27 +-
 src/web/app/common/mios.ts                    |  19 +-
 .../common/scripts/streaming/home-stream.ts   |   4 +-
 .../desktop/-tags/home-widgets/channel.tag    | 318 ------------------
 src/web/app/desktop/script.ts                 |   3 +
 .../app/desktop/views/components/calendar.vue |   2 +-
 src/web/app/desktop/views/components/home.vue |  47 +--
 src/web/app/desktop/views/components/index.ts |   2 +
 .../views/components/widgets/activity.vue     |   4 +-
 .../views/components/widgets/broadcast.vue    |   4 +-
 .../views/components/widgets/calendar.vue     |   4 +-
 .../widgets/channel.channel.form.vue          |  67 ++++
 .../widgets/channel.channel.post.vue          |  64 ++++
 .../components/widgets/channel.channel.vue    | 104 ++++++
 .../views/components/widgets/channel.vue      | 107 ++++++
 .../views/components/widgets/messaging.vue    |   4 +-
 .../components/widgets/notifications.vue      |   4 +-
 .../views/components/widgets/photo-stream.vue |   4 +-
 .../views/components/widgets/polls.vue        |   4 +-
 .../views/components/widgets/post-form.vue    |   4 +-
 .../views/components/widgets/profile.vue      |  18 +-
 .../desktop/views/components/widgets/rss.vue  |   4 +-
 .../views/components/widgets/server.vue       |   4 +-
 .../views/components/widgets/slideshow.vue    |   4 +-
 .../views/components/widgets/timemachine.vue  |   4 +-
 .../views/components/widgets/trends.vue       |   4 +-
 .../views/components/widgets/users.vue        |   4 +-
 .../{home-custmize.vue => home-customize.vue} |   2 +-
 src/web/app/init.ts                           |   8 +
 29 files changed, 422 insertions(+), 426 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/channel.tag
 create mode 100644 src/web/app/desktop/views/components/widgets/channel.channel.form.vue
 create mode 100644 src/web/app/desktop/views/components/widgets/channel.channel.post.vue
 create mode 100644 src/web/app/desktop/views/components/widgets/channel.channel.vue
 create mode 100644 src/web/app/desktop/views/components/widgets/channel.vue
 rename src/web/app/desktop/views/pages/{home-custmize.vue => home-customize.vue} (89%)

diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index 6088efd7e..930a7c586 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -2,7 +2,7 @@ import Vue from 'vue';
 
 export default function<T extends object>(data: {
 	name: string;
-	props?: T;
+	props?: () => T;
 }) {
 	return Vue.extend({
 		props: {
@@ -17,20 +17,9 @@ export default function<T extends object>(data: {
 		},
 		data() {
 			return {
-				props: data.props || {} as T
+				props: data.props ? data.props() : {} as T
 			};
 		},
-		watch: {
-			props(newProps, oldProps) {
-				if (JSON.stringify(newProps) == JSON.stringify(oldProps)) return;
-				(this as any).api('i/update_home', {
-					id: this.id,
-					data: newProps
-				}).then(() => {
-					(this as any).os.i.client_settings.home.find(w => w.id == this.id).data = newProps;
-				});
-			}
-		},
 		created() {
 			if (this.props) {
 				Object.keys(this.props).forEach(prop => {
@@ -39,6 +28,18 @@ export default function<T extends object>(data: {
 					}
 				});
 			}
+
+			this.$watch('props', newProps => {
+				console.log(this.id, newProps);
+				(this as any).api('i/update_home', {
+					id: this.id,
+					data: newProps
+				}).then(() => {
+					(this as any).os.i.client_settings.home.find(w => w.id == this.id).data = newProps;
+				});
+			}, {
+				deep: true
+			});
 		}
 	});
 }
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index c4208aa91..4b9375f54 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -1,9 +1,8 @@
 import { EventEmitter } from 'eventemitter3';
-import * as riot from 'riot';
+import api from './scripts/api';
 import signout from './scripts/signout';
 import Progress from './scripts/loading';
 import HomeStreamManager from './scripts/streaming/home-stream-manager';
-import api from './scripts/api';
 import DriveStreamManager from './scripts/streaming/drive-stream-manager';
 import ServerStreamManager from './scripts/streaming/server-stream-manager';
 import RequestsStreamManager from './scripts/streaming/requests-stream-manager';
@@ -226,22 +225,8 @@ export default class MiOS extends EventEmitter {
 		// フェッチが完了したとき
 		const fetched = me => {
 			if (me) {
-				riot.observable(me);
-
-				// この me オブジェクトを更新するメソッド
-				me.update = data => {
-					if (data) Object.assign(me, data);
-					me.trigger('updated');
-				};
-
 				// ローカルストレージにキャッシュ
 				localStorage.setItem('me', JSON.stringify(me));
-
-				// 自分の情報が更新されたとき
-				me.on('updated', () => {
-					// キャッシュ更新
-					localStorage.setItem('me', JSON.stringify(me));
-				});
 			}
 
 			this.i = me;
@@ -270,8 +255,6 @@ export default class MiOS extends EventEmitter {
 			// 後から新鮮なデータをフェッチ
 			fetchme(cachedMe.token, freshData => {
 				Object.assign(cachedMe, freshData);
-				cachedMe.trigger('updated');
-				cachedMe.trigger('refreshed');
 			});
 		} else {
 			// Get token from cookie
diff --git a/src/web/app/common/scripts/streaming/home-stream.ts b/src/web/app/common/scripts/streaming/home-stream.ts
index 11ad754ef..a92b61cae 100644
--- a/src/web/app/common/scripts/streaming/home-stream.ts
+++ b/src/web/app/common/scripts/streaming/home-stream.ts
@@ -16,7 +16,9 @@ export default class Connection extends Stream {
 		}, 1000 * 60);
 
 		// 自分の情報が更新されたとき
-		this.on('i_updated', me.update);
+		this.on('i_updated', i => {
+			Object.assign(me, i);
+		});
 
 		// トークンが再生成されたとき
 		// このままではAPIが利用できないので強制的にサインアウトさせる
diff --git a/src/web/app/desktop/-tags/home-widgets/channel.tag b/src/web/app/desktop/-tags/home-widgets/channel.tag
deleted file mode 100644
index c20a851e7..000000000
--- a/src/web/app/desktop/-tags/home-widgets/channel.tag
+++ /dev/null
@@ -1,318 +0,0 @@
-<mk-channel-home-widget>
-	<template v-if="!data.compact">
-		<p class="title">%fa:tv%{
-			channel ? channel.title : '%i18n:desktop.tags.mk-channel-home-widget.title%'
-		}</p>
-		<button @click="settings" title="%i18n:desktop.tags.mk-channel-home-widget.settings%">%fa:cog%</button>
-	</template>
-	<p class="get-started" v-if="this.data.channel == null">%i18n:desktop.tags.mk-channel-home-widget.get-started%</p>
-	<mk-channel ref="channel" show={ this.data.channel }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-			overflow hidden
-
-			> .title
-				z-index 2
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> [data-fa]
-					margin-right 4px
-
-			> button
-				position absolute
-				z-index 2
-				top 0
-				right 0
-				padding 0
-				width 42px
-				font-size 0.9em
-				line-height 42px
-				color #ccc
-
-				&:hover
-					color #aaa
-
-				&:active
-					color #999
-
-			> .get-started
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-			> mk-channel
-				height 200px
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			channel: null,
-			compact: false
-		};
-
-		this.mixin('widget');
-
-		this.on('mount', () => {
-			if (this.data.channel) {
-				this.zap();
-			}
-		});
-
-		this.zap = () => {
-			this.update({
-				fetching: true
-			});
-
-			this.$root.$data.os.api('channels/show', {
-				channel_id: this.data.channel
-			}).then(channel => {
-				this.update({
-					fetching: false,
-					channel: channel
-				});
-
-				this.$refs.channel.zap(channel);
-			});
-		};
-
-		this.settings = () => {
-			const id = window.prompt('チャンネルID');
-			if (!id) return;
-			this.data.channel = id;
-			this.zap();
-
-			// Save state
-			this.save();
-		};
-
-		this.func = () => {
-			this.data.compact = !this.data.compact;
-			this.save();
-		};
-	</script>
-</mk-channel-home-widget>
-
-<mk-channel>
-	<p v-if="fetching">読み込み中<mk-ellipsis/></p>
-	<div v-if="!fetching" ref="posts">
-		<p v-if="posts.length == 0">まだ投稿がありません</p>
-		<mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/>
-	</div>
-	<mk-channel-form ref="form"/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> p
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-			> div
-				height calc(100% - 38px)
-				overflow auto
-				font-size 0.9em
-
-				> mk-channel-post
-					border-bottom solid 1px #eee
-
-					&:last-child
-						border-bottom none
-
-			> mk-channel-form
-				position absolute
-				left 0
-				bottom 0
-
-	</style>
-	<script lang="typescript">
-		import ChannelStream from '../../../common/scripts/streaming/channel-stream';
-
-		this.mixin('api');
-
-		this.fetching = true;
-		this.channel = null;
-		this.posts = [];
-
-		this.on('unmount', () => {
-			if (this.connection) {
-				this.connection.off('post', this.onPost);
-				this.connection.close();
-			}
-		});
-
-		this.zap = channel => {
-			this.update({
-				fetching: true,
-				channel: channel
-			});
-
-			this.$root.$data.os.api('channels/posts', {
-				channel_id: channel.id
-			}).then(posts => {
-				this.update({
-					fetching: false,
-					posts: posts
-				});
-
-				this.scrollToBottom();
-
-				if (this.connection) {
-					this.connection.off('post', this.onPost);
-					this.connection.close();
-				}
-				this.connection = new ChannelStream(this.channel.id);
-				this.connection.on('post', this.onPost);
-			});
-		};
-
-		this.onPost = post => {
-			this.posts.unshift(post);
-			this.update();
-			this.scrollToBottom();
-		};
-
-		this.scrollToBottom = () => {
-			this.$refs.posts.scrollTop = this.$refs.posts.scrollHeight;
-		};
-	</script>
-</mk-channel>
-
-<mk-channel-post>
-	<header>
-		<a class="index" @click="reply">{ post.index }:</a>
-		<a class="name" href={ _URL_ + '/' + post.user.username }><b>{ post.user.name }</b></a>
-		<span>ID:<i>{ post.user.username }</i></span>
-	</header>
-	<div>
-		<a v-if="post.reply">&gt;&gt;{ post.reply.index }</a>
-		{ post.text }
-		<div class="media" v-if="post.media">
-			<template each={ file in post.media }>
-				<a href={ file.url } target="_blank">
-					<img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
-				</a>
-			</template>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 0
-			color #444
-
-			> header
-				position -webkit-sticky
-				position sticky
-				z-index 1
-				top 0
-				padding 8px 4px 4px 16px
-				background rgba(255, 255, 255, 0.9)
-
-				> .index
-					margin-right 0.25em
-
-				> .name
-					margin-right 0.5em
-					color #008000
-
-			> div
-				padding 0 16px 16px 16px
-
-				> .media
-					> a
-						display inline-block
-
-						> img
-							max-width 100%
-							vertical-align bottom
-
-	</style>
-	<script lang="typescript">
-		this.post = this.opts.post;
-		this.form = this.opts.form;
-
-		this.reply = () => {
-			this.form.refs.text.value = `>>${ this.post.index } `;
-		};
-	</script>
-</mk-channel-post>
-
-<mk-channel-form>
-	<input ref="text" disabled={ wait } onkeydown={ onkeydown } placeholder="書いて">
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			width 100%
-			height 38px
-			padding 4px
-			border-top solid 1px #ddd
-
-			> input
-				padding 0 8px
-				width 100%
-				height 100%
-				font-size 14px
-				color #55595c
-				border solid 1px #dadada
-				border-radius 4px
-
-				&:hover
-				&:focus
-					border-color #aeaeae
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.clear = () => {
-			this.$refs.text.value = '';
-		};
-
-		this.onkeydown = e => {
-			if (e.which == 10 || e.which == 13) this.post();
-		};
-
-		this.post = () => {
-			this.update({
-				wait: true
-			});
-
-			let text = this.$refs.text.value;
-			let reply = null;
-
-			if (/^>>([0-9]+) /.test(text)) {
-				const index = text.match(/^>>([0-9]+) /)[1];
-				reply = this.parent.posts.find(p => p.index.toString() == index);
-				text = text.replace(/^>>([0-9]+) /, '');
-			}
-
-			this.$root.$data.os.api('posts/create', {
-				text: text,
-				reply_id: reply ? reply.id : undefined,
-				channel_id: this.parent.channel.id
-			}).then(data => {
-				this.clear();
-			}).catch(err => {
-				alert('失敗した');
-			}).then(() => {
-				this.update({
-					wait: false
-				});
-			});
-		};
-	</script>
-</mk-channel-form>
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index b647f4031..4f2ac61ee 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -23,6 +23,7 @@ import MkIndex from './views/pages/index.vue';
 import MkUser from './views/pages/user/user.vue';
 import MkSelectDrive from './views/pages/selectdrive.vue';
 import MkDrive from './views/pages/drive.vue';
+import MkHomeCustomize from './views/pages/home-customize.vue';
 
 /**
  * init
@@ -66,6 +67,8 @@ init(async (launch) => {
 
 	app.$router.addRoutes([{
 		path: '/', name: 'index', component: MkIndex
+	}, {
+		path: '/i/customize-home', component: MkHomeCustomize
 	}, {
 		path: '/i/drive', component: MkDrive
 	}, {
diff --git a/src/web/app/desktop/views/components/calendar.vue b/src/web/app/desktop/views/components/calendar.vue
index a21d3e614..08b08f8d4 100644
--- a/src/web/app/desktop/views/components/calendar.vue
+++ b/src/web/app/desktop/views/components/calendar.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-calendar">
+<div class="mk-calendar" :data-melt="design == 4 || design == 5">
 	<template v-if="design == 0 || design == 1">
 		<button @click="prev" title="%i18n:desktop.tags.mk-calendar-widget.prev%">%fa:chevron-circle-left%</button>
 		<p class="title">{{ '%i18n:desktop.tags.mk-calendar-widget.title%'.replace('{1}', year).replace('{2}', month) }}</p>
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 8e64a2d83..6ab1512b0 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -40,7 +40,7 @@
 		<div v-for="place in ['left', 'main', 'right']" :class="place" :ref="place" :data-place="place">
 			<template v-if="place != 'main'">
 				<template v-for="widget in widgets[place]">
-					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
+					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)" :data-widget-id="widget.id">
 						<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id"/>
 					</div>
 					<template v-else>
@@ -60,7 +60,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as uuid from 'uuid';
-import Sortable from 'sortablejs';
+import * as Sortable from 'sortablejs';
 
 export default Vue.extend({
 	props: {
@@ -72,7 +72,6 @@ export default Vue.extend({
 	},
 	data() {
 		return {
-			home: [],
 			bakedHomeData: null,
 			widgetAdderSelected: null
 		};
@@ -95,16 +94,15 @@ export default Vue.extend({
 		},
 		rightEl(): Element {
 			return (this.$refs.right as Element[])[0];
+		},
+		home(): any {
+			return (this as any).os.i.client_settings.home;
 		}
 	},
 	created() {
 		this.bakedHomeData = this.bakeHomeData();
 	},
 	mounted() {
-		(this as any).os.i.on('refreshed', this.onMeRefreshed);
-
-		this.home = (this as any).os.i.client_settings.home;
-
 		this.$nextTick(() => {
 			if (!this.customize) {
 				if (this.leftEl.children.length == 0) {
@@ -132,7 +130,7 @@ export default Vue.extend({
 					animation: 150,
 					onMove: evt => {
 						const id = evt.dragged.getAttribute('data-widget-id');
-						this.home.find(tag => tag.id == id).widget.place = evt.to.getAttribute('data-place');
+						this.home.find(w => w.id == id).place = evt.to.getAttribute('data-place');
 					},
 					onSort: () => {
 						this.saveHome();
@@ -153,24 +151,15 @@ export default Vue.extend({
 			}
 		});
 	},
-	beforeDestroy() {
-		(this as any).os.i.off('refreshed', this.onMeRefreshed);
-	},
 	methods: {
 		bakeHomeData() {
-			return JSON.stringify((this as any).os.i.client_settings.home);
+			return JSON.stringify(this.home);
 		},
 		onTlLoaded() {
 			this.$emit('loaded');
 		},
-		onMeRefreshed() {
-			if (this.bakedHomeData != this.bakeHomeData()) {
-				// TODO: i18n
-				alert('別の場所でホームが編集されました。ページを再度読み込みすると編集が反映されます。');
-			}
-		},
 		onWidgetContextmenu(widgetId) {
-			(this.$refs[widgetId] as any).func();
+			(this.$refs[widgetId] as any)[0].func();
 		},
 		addWidget() {
 			const widget = {
@@ -180,29 +169,13 @@ export default Vue.extend({
 				data: {}
 			};
 
-			(this as any).os.i.client_settings.home.unshift(widget);
+			this.home.unshift(widget);
 
 			this.saveHome();
 		},
 		saveHome() {
-			const data = [];
-
-			Array.from(this.leftEl.children).forEach(el => {
-				const id = el.getAttribute('data-widget-id');
-				const widget = (this as any).os.i.client_settings.home.find(w => w.id == id);
-				widget.place = 'left';
-				data.push(widget);
-			});
-
-			Array.from(this.rightEl.children).forEach(el => {
-				const id = el.getAttribute('data-widget-id');
-				const widget = (this as any).os.i.client_settings.home.find(w => w.id == id);
-				widget.place = 'right';
-				data.push(widget);
-			});
-
 			(this as any).api('i/update_home', {
-				home: data
+				home: this.home
 			});
 		},
 		warp(date) {
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index cbe145daf..86606a14a 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -35,6 +35,7 @@ import wDonation from './widgets/donation.vue';
 import wNotifications from './widgets/notifications.vue';
 import wBroadcast from './widgets/broadcast.vue';
 import wTimemachine from './widgets/timemachine.vue';
+import wProfile from './widgets/profile.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-notification', uiNotification);
@@ -71,3 +72,4 @@ Vue.component('mkw-donation', wDonation);
 Vue.component('mkw-notifications', wNotifications);
 Vue.component('mkw-broadcast', wBroadcast);
 Vue.component('mkw-timemachine', wTimemachine);
+Vue.component('mkw-profile', wProfile);
diff --git a/src/web/app/desktop/views/components/widgets/activity.vue b/src/web/app/desktop/views/components/widgets/activity.vue
index 8bf45a556..2ff5fe4f0 100644
--- a/src/web/app/desktop/views/components/widgets/activity.vue
+++ b/src/web/app/desktop/views/components/widgets/activity.vue
@@ -10,10 +10,10 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'activity',
-	props: {
+	props: () => ({
 		design: 0,
 		view: 0
-	}
+	})
 }).extend({
 	methods: {
 		func() {
diff --git a/src/web/app/desktop/views/components/widgets/broadcast.vue b/src/web/app/desktop/views/components/widgets/broadcast.vue
index 1a0fd9280..68c9cebfa 100644
--- a/src/web/app/desktop/views/components/widgets/broadcast.vue
+++ b/src/web/app/desktop/views/components/widgets/broadcast.vue
@@ -25,9 +25,9 @@ import { lang } from '../../../../config';
 
 export default define({
 	name: 'broadcast',
-	props: {
+	props: () => ({
 		design: 0
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/calendar.vue b/src/web/app/desktop/views/components/widgets/calendar.vue
index 8574bf59f..c16602db4 100644
--- a/src/web/app/desktop/views/components/widgets/calendar.vue
+++ b/src/web/app/desktop/views/components/widgets/calendar.vue
@@ -38,9 +38,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'calendar',
-	props: {
+	props: () => ({
 		design: 0
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/channel.channel.form.vue b/src/web/app/desktop/views/components/widgets/channel.channel.form.vue
new file mode 100644
index 000000000..392ba5924
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/channel.channel.form.vue
@@ -0,0 +1,67 @@
+<template>
+<div class="form">
+	<input v-model="text" :disabled="wait" @keydown="onKeydown" placeholder="書いて">
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			text: '',
+			wait: false
+		};
+	},
+	methods: {
+		onKeydown(e) {
+			if (e.which == 10 || e.which == 13) this.post();
+		},
+		post() {
+			this.wait = true;
+
+			let reply = null;
+
+			if (/^>>([0-9]+) /.test(this.text)) {
+				const index = this.text.match(/^>>([0-9]+) /)[1];
+				reply = (this.$parent as any).posts.find(p => p.index.toString() == index);
+				this.text = this.text.replace(/^>>([0-9]+) /, '');
+			}
+
+			(this as any).api('posts/create', {
+				text: this.text,
+				reply_id: reply ? reply.id : undefined,
+				channel_id: (this.$parent as any).channel.id
+			}).then(data => {
+				this.text = '';
+			}).catch(err => {
+				alert('失敗した');
+			}).then(() => {
+				this.wait = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.form
+	width 100%
+	height 38px
+	padding 4px
+	border-top solid 1px #ddd
+
+	> input
+		padding 0 8px
+		width 100%
+		height 100%
+		font-size 14px
+		color #55595c
+		border solid 1px #dadada
+		border-radius 4px
+
+		&:hover
+		&:focus
+			border-color #aeaeae
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/channel.channel.post.vue b/src/web/app/desktop/views/components/widgets/channel.channel.post.vue
new file mode 100644
index 000000000..faaf0fb73
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/channel.channel.post.vue
@@ -0,0 +1,64 @@
+<template>
+<div class="post">
+	<header>
+		<a class="index" @click="reply">{{ post.index }}:</a>
+		<router-link class="name" :to="`/${post.user.username}`" v-user-preview="post.user.id"><b>{{ post.user.name }}</b></router-link>
+		<span>ID:<i>{{ post.user.username }}</i></span>
+	</header>
+	<div>
+		<a v-if="post.reply">&gt;&gt;{{ post.reply.index }}</a>
+		{{ post.text }}
+		<div class="media" v-if="post.media">
+			<a v-for="file in post.media" :href="file.url" target="_blank">
+				<img :src="`${file.url}?thumbnail&size=512`" :alt="file.name" :title="file.name"/>
+			</a>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['post'],
+	methods: {
+		reply() {
+			this.$emit('reply', this.post);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.post
+	margin 0
+	padding 0
+	color #444
+
+	> header
+		position -webkit-sticky
+		position sticky
+		z-index 1
+		top 0
+		padding 8px 4px 4px 16px
+		background rgba(255, 255, 255, 0.9)
+
+		> .index
+			margin-right 0.25em
+
+		> .name
+			margin-right 0.5em
+			color #008000
+
+	> div
+		padding 0 16px 16px 16px
+
+		> .media
+			> a
+				display inline-block
+
+				> img
+					max-width 100%
+					vertical-align bottom
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/channel.channel.vue b/src/web/app/desktop/views/components/widgets/channel.channel.vue
new file mode 100644
index 000000000..5de13aec0
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/channel.channel.vue
@@ -0,0 +1,104 @@
+<template>
+<div class="channel">
+	<p v-if="fetching">読み込み中<mk-ellipsis/></p>
+	<div v-if="!fetching" ref="posts">
+		<p v-if="posts.length == 0">まだ投稿がありません</p>
+		<x-post class="post" v-for="post in posts.slice().reverse()" :post="post" :key="post.id" @reply="reply"/>
+	</div>
+	<x-form class="form" ref="form"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import ChannelStream from '../../../../common/scripts/streaming/channel-stream';
+import XForm from './channel.channel.form.vue';
+import XPost from './channel.channel.post.vue';
+
+export default Vue.extend({
+	components: {
+		XForm,
+		XPost
+	},
+	props: ['channel'],
+	data() {
+		return {
+			fetching: true,
+			posts: [],
+			connection: null
+		};
+	},
+	watch: {
+		channel() {
+			this.zap();
+		}
+	},
+	mounted() {
+		this.zap();
+	},
+	beforeDestroy() {
+		this.disconnect();
+	},
+	methods: {
+		zap() {
+			this.fetching = true;
+
+			(this as any).api('channels/posts', {
+				channel_id: this.channel.id
+			}).then(posts => {
+				this.posts = posts;
+				this.fetching = false;
+
+				this.scrollToBottom();
+
+				this.disconnect();
+				this.connection = new ChannelStream(this.channel.id);
+				this.connection.on('post', this.onPost);
+			});
+		},
+		disconnect() {
+			if (this.connection) {
+				this.connection.off('post', this.onPost);
+				this.connection.close();
+			}
+		},
+		onPost(post) {
+			this.posts.unshift(post);
+			this.scrollToBottom();
+		},
+		scrollToBottom() {
+			(this.$refs.posts as any).scrollTop = (this.$refs.posts as any).scrollHeight;
+		},
+		reply(post) {
+			(this.$refs.form as any).text = `>>${ post.index } `;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.channel
+
+	> p
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+	> div
+		height calc(100% - 38px)
+		overflow auto
+		font-size 0.9em
+
+		> .post
+			border-bottom solid 1px #eee
+
+			&:last-child
+				border-bottom none
+
+	> .form
+		position absolute
+		left 0
+		bottom 0
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/channel.vue b/src/web/app/desktop/views/components/widgets/channel.vue
new file mode 100644
index 000000000..484dca9f6
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/channel.vue
@@ -0,0 +1,107 @@
+<template>
+<div class="mkw-channel">
+	<template v-if="!data.compact">
+		<p class="title">%fa:tv%{{ channel ? channel.title : '%i18n:desktop.tags.mk-channel-home-widget.title%' }}</p>
+		<button @click="settings" title="%i18n:desktop.tags.mk-channel-home-widget.settings%">%fa:cog%</button>
+	</template>
+	<p class="get-started" v-if="props.channel == null">%i18n:desktop.tags.mk-channel-home-widget.get-started%</p>
+	<x-channel class="channel" :channel="channel" v-else/>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+import XChannel from './channel.channel.vue';
+
+export default define({
+	name: 'server',
+	props: () => ({
+		channel: null,
+		compact: false
+	})
+}).extend({
+	components: {
+		XChannel
+	},
+	data() {
+		return {
+			fetching: true,
+			channel: null
+		};
+	},
+	mounted() {
+		if (this.props.channel) {
+				this.zap();
+			}
+	},
+	methods: {
+		func() {
+			this.props.compact = !this.props.compact;
+		},
+		settings() {
+			const id = window.prompt('チャンネルID');
+			if (!id) return;
+			this.props.channel = id;
+			this.zap();
+		},
+		zap() {
+			this.fetching = true;
+
+			(this as any).api('channels/show', {
+				channel_id: this.props.channel
+			}).then(channel => {
+				this.channel = channel;
+				this.fetching = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-channel
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+	overflow hidden
+
+	> .title
+		z-index 2
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> [data-fa]
+			margin-right 4px
+
+	> button
+		position absolute
+		z-index 2
+		top 0
+		right 0
+		padding 0
+		width 42px
+		font-size 0.9em
+		line-height 42px
+		color #ccc
+
+		&:hover
+			color #aaa
+
+		&:active
+			color #999
+
+	> .get-started
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+	> .channel
+		height 200px
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/messaging.vue b/src/web/app/desktop/views/components/widgets/messaging.vue
index 733989b78..039a524f5 100644
--- a/src/web/app/desktop/views/components/widgets/messaging.vue
+++ b/src/web/app/desktop/views/components/widgets/messaging.vue
@@ -9,9 +9,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'messaging',
-	props: {
+	props: () => ({
 		design: 0
-	}
+	})
 }).extend({
 	methods: {
 		navigate(user) {
diff --git a/src/web/app/desktop/views/components/widgets/notifications.vue b/src/web/app/desktop/views/components/widgets/notifications.vue
index 2d613fa23..978cf5218 100644
--- a/src/web/app/desktop/views/components/widgets/notifications.vue
+++ b/src/web/app/desktop/views/components/widgets/notifications.vue
@@ -12,9 +12,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'notifications',
-	props: {
+	props: () => ({
 		compact: false
-	}
+	})
 }).extend({
 	methods: {
 		settings() {
diff --git a/src/web/app/desktop/views/components/widgets/photo-stream.vue b/src/web/app/desktop/views/components/widgets/photo-stream.vue
index 6ad7d2f06..04b71975b 100644
--- a/src/web/app/desktop/views/components/widgets/photo-stream.vue
+++ b/src/web/app/desktop/views/components/widgets/photo-stream.vue
@@ -13,9 +13,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'photo-stream',
-	props: {
+	props: () => ({
 		design: 0
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/polls.vue b/src/web/app/desktop/views/components/widgets/polls.vue
index 71d5391b1..f1b34ceed 100644
--- a/src/web/app/desktop/views/components/widgets/polls.vue
+++ b/src/web/app/desktop/views/components/widgets/polls.vue
@@ -18,9 +18,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'polls',
-	props: {
+	props: () => ({
 		compact: false
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/post-form.vue b/src/web/app/desktop/views/components/widgets/post-form.vue
index c32ad5761..94b03f84a 100644
--- a/src/web/app/desktop/views/components/widgets/post-form.vue
+++ b/src/web/app/desktop/views/components/widgets/post-form.vue
@@ -12,9 +12,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'post-form',
-	props: {
+	props: () => ({
 		design: 0
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/profile.vue b/src/web/app/desktop/views/components/widgets/profile.vue
index 9a0d40a5c..68cf46978 100644
--- a/src/web/app/desktop/views/components/widgets/profile.vue
+++ b/src/web/app/desktop/views/components/widgets/profile.vue
@@ -4,19 +4,19 @@
 	:data-melt="props.design == 2"
 >
 	<div class="banner"
-		style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=256)' : '' }
+		:style="os.i.banner_url ? `background-image: url(${os.i.banner_url}?thumbnail&size=256)` : ''"
 		title="クリックでバナー編集"
-		@click="wapi_setBanner"
+		@click="os.apis.updateBanner"
 	></div>
 	<img class="avatar"
-		src={ I.avatar_url + '?thumbnail&size=96' }
-		@click="wapi_setAvatar"
+		:src="`${os.i.avatar_url}?thumbnail&size=96`"
+		@click="os.apis.updateAvatar"
 		alt="avatar"
 		title="クリックでアバター編集"
-		v-user-preview={ I.id }
+		v-user-preview="os.i.id"
 	/>
-	<a class="name" href={ '/' + I.username }>{ I.name }</a>
-	<p class="username">@{ I.username }</p>
+	<router-link class="name" :to="`/${os.i.username}`">{{ os.i.name }}</router-link>
+	<p class="username">@{{ os.i.username }}</p>
 </div>
 </template>
 
@@ -24,9 +24,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'profile',
-	props: {
+	props: () => ({
 		design: 0
-	}
+	})
 }).extend({
 	methods: {
 		func() {
diff --git a/src/web/app/desktop/views/components/widgets/rss.vue b/src/web/app/desktop/views/components/widgets/rss.vue
index 954edf3c5..350712971 100644
--- a/src/web/app/desktop/views/components/widgets/rss.vue
+++ b/src/web/app/desktop/views/components/widgets/rss.vue
@@ -15,9 +15,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'rss',
-	props: {
+	props: () => ({
 		compact: false
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/server.vue b/src/web/app/desktop/views/components/widgets/server.vue
index 00e2f8f18..c08056691 100644
--- a/src/web/app/desktop/views/components/widgets/server.vue
+++ b/src/web/app/desktop/views/components/widgets/server.vue
@@ -27,10 +27,10 @@ import XInfo from './server.info.vue';
 
 export default define({
 	name: 'server',
-	props: {
+	props: () => ({
 		design: 0,
 		view: 0
-	}
+	})
 }).extend({
 	components: {
 		XCpuMemory,
diff --git a/src/web/app/desktop/views/components/widgets/slideshow.vue b/src/web/app/desktop/views/components/widgets/slideshow.vue
index 3c2ef6da4..75af3c0f1 100644
--- a/src/web/app/desktop/views/components/widgets/slideshow.vue
+++ b/src/web/app/desktop/views/components/widgets/slideshow.vue
@@ -15,10 +15,10 @@ import * as anime from 'animejs';
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'slideshow',
-	props: {
+	props: () => ({
 		folder: undefined,
 		size: 0
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/timemachine.vue b/src/web/app/desktop/views/components/widgets/timemachine.vue
index d484ce6d7..742048216 100644
--- a/src/web/app/desktop/views/components/widgets/timemachine.vue
+++ b/src/web/app/desktop/views/components/widgets/timemachine.vue
@@ -8,9 +8,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'timemachine',
-	props: {
+	props: () => ({
 		design: 0
-	}
+	})
 }).extend({
 	methods: {
 		chosen(date) {
diff --git a/src/web/app/desktop/views/components/widgets/trends.vue b/src/web/app/desktop/views/components/widgets/trends.vue
index 23d39563f..a764639ce 100644
--- a/src/web/app/desktop/views/components/widgets/trends.vue
+++ b/src/web/app/desktop/views/components/widgets/trends.vue
@@ -17,9 +17,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'trends',
-	props: {
+	props: () => ({
 		compact: false
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/users.vue b/src/web/app/desktop/views/components/widgets/users.vue
index 6876d0bf0..4a9ab2aa3 100644
--- a/src/web/app/desktop/views/components/widgets/users.vue
+++ b/src/web/app/desktop/views/components/widgets/users.vue
@@ -28,9 +28,9 @@ const limit = 3;
 
 export default define({
 	name: 'users',
-	props: {
+	props: () => ({
 		compact: false
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/pages/home-custmize.vue b/src/web/app/desktop/views/pages/home-customize.vue
similarity index 89%
rename from src/web/app/desktop/views/pages/home-custmize.vue
rename to src/web/app/desktop/views/pages/home-customize.vue
index 257e83cad..8aa06be57 100644
--- a/src/web/app/desktop/views/pages/home-custmize.vue
+++ b/src/web/app/desktop/views/pages/home-customize.vue
@@ -1,5 +1,5 @@
 <template>
-	<mk-home customize/>
+<mk-home customize/>
 </template>
 
 <script lang="ts">
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 02c125efe..e4cb8f8bc 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -103,6 +103,14 @@ export default (callback: (launch: (api: (os: MiOS) => API) => [Vue, MiOS]) => v
 				router: new VueRouter({
 					mode: 'history'
 				}),
+				created() {
+					this.$watch('os.i', i => {
+						// キャッシュ更新
+						localStorage.setItem('me', JSON.stringify(i));
+					}, {
+						deep: true
+					});
+				},
 				render: createEl => createEl(App)
 			}).$mount('#app');
 

From c0f566f828914252180b74f8d3fedd21ebd19da3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 15:30:56 +0900
Subject: [PATCH 238/286] wip

---
 src/web/app/desktop/views/components/home.vue | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 6ab1512b0..011c1fe85 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -72,7 +72,6 @@ export default Vue.extend({
 	},
 	data() {
 		return {
-			bakedHomeData: null,
 			widgetAdderSelected: null
 		};
 	},
@@ -99,9 +98,6 @@ export default Vue.extend({
 			return (this as any).os.i.client_settings.home;
 		}
 	},
-	created() {
-		this.bakedHomeData = this.bakeHomeData();
-	},
 	mounted() {
 		this.$nextTick(() => {
 			if (!this.customize) {
@@ -152,9 +148,6 @@ export default Vue.extend({
 		});
 	},
 	methods: {
-		bakeHomeData() {
-			return JSON.stringify(this.home);
-		},
 		onTlLoaded() {
 			this.$emit('loaded');
 		},

From b4efb01be3f7ca41339f3c995558e9db6e906c7b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 20:55:03 +0900
Subject: [PATCH 239/286] wip

---
 src/web/app/desktop/views/components/home.vue | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 011c1fe85..48aa5e3ea 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -76,11 +76,21 @@ export default Vue.extend({
 		};
 	},
 	computed: {
+		home(): any {
+			//#region 互換性のため
+			(this as any).os.i.client_settings.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.client_settings.home;
+		},
 		leftWidgets(): any {
-			return (this as any).os.i.client_settings.home.filter(w => w.place == 'left');
+			return this.home.filter(w => w.place == 'left');
 		},
 		rightWidgets(): any {
-			return (this as any).os.i.client_settings.home.filter(w => w.place == 'right');
+			return this.home.filter(w => w.place == 'right');
 		},
 		widgets(): any {
 			return {
@@ -93,9 +103,6 @@ export default Vue.extend({
 		},
 		rightEl(): Element {
 			return (this.$refs.right as Element[])[0];
-		},
-		home(): any {
-			return (this as any).os.i.client_settings.home;
 		}
 	},
 	mounted() {
@@ -140,7 +147,7 @@ export default Vue.extend({
 						const el = evt.item;
 						const id = el.getAttribute('data-widget-id');
 						el.parentNode.removeChild(el);
-						(this as any).os.i.client_settings.home = (this as any).os.i.client_settings.home.filter(w => w.id != id);
+						(this as any).os.i.client_settings.home = this.home.filter(w => w.id != id);
 						this.saveHome();
 					}
 				}));

From dd72410b1dcbbf2aa5e74278589522030e976efc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 21:02:39 +0900
Subject: [PATCH 240/286] wip

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

diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 48aa5e3ea..9d2198d9d 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -147,7 +147,7 @@ export default Vue.extend({
 						const el = evt.item;
 						const id = el.getAttribute('data-widget-id');
 						el.parentNode.removeChild(el);
-						(this as any).os.i.client_settings.home = this.home.filter(w => w.id != id);
+						this.home = this.home.filter(w => w.id != id);
 						this.saveHome();
 					}
 				}));

From 984294661039353c1346e19a26aaf9fcab517194 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 21:08:03 +0900
Subject: [PATCH 241/286] wip

---
 src/web/app/common/-tags/activity-table.tag | 57 ---------------------
 src/web/app/mobile/views/pages/user.vue     |  7 +--
 2 files changed, 2 insertions(+), 62 deletions(-)
 delete mode 100644 src/web/app/common/-tags/activity-table.tag

diff --git a/src/web/app/common/-tags/activity-table.tag b/src/web/app/common/-tags/activity-table.tag
deleted file mode 100644
index cd74b0920..000000000
--- a/src/web/app/common/-tags/activity-table.tag
+++ /dev/null
@@ -1,57 +0,0 @@
-<mk-activity-table>
-	<svg v-if="data" ref="canvas" viewBox="0 0 53 7" preserveAspectRatio="none">
-		<rect each={ data } width="1" height="1"
-			riot-x={ x } riot-y={ date.weekday }
-			rx="1" ry="1"
-			fill={ color }
-			style="transform: scale({ v });"/>
-		<rect class="today" width="1" height="1"
-			riot-x={ data[data.length - 1].x } riot-y={ data[data.length - 1].date.weekday }
-			rx="1" ry="1"
-			fill="none"
-			stroke-width="0.1"
-			stroke="#f73520"/>
-	</svg>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			max-width 600px
-			margin 0 auto
-
-			> svg
-				display block
-
-				> rect
-					transform-origin center
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.user = this.opts.user;
-
-		this.on('mount', () => {
-			this.$root.$data.os.api('aggregation/users/activity', {
-				user_id: this.user.id
-			}).then(data => {
-				data.forEach(d => d.total = d.posts + d.replies + d.reposts);
-				this.peak = Math.max.apply(null, data.map(d => d.total)) / 2;
-				let x = 0;
-				data.reverse().forEach(d => {
-					d.x = x;
-					d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay();
-
-					d.v = d.total / this.peak;
-					if (d.v > 1) d.v = 1;
-					const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
-					const cs = d.v * 100;
-					const cl = 15 + ((1 - d.v) * 80);
-					d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
-
-					if (d.date.weekday == 6) x++;
-				});
-				this.update({ data });
-			});
-		});
-	</script>
-</mk-activity-table>
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index 53cde1fb6..745de2c6e 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -26,8 +26,8 @@
 					</p>
 				</div>
 				<div class="status">
-				  <a>
-				    <b>{{ user.posts_count }}</b>
+					<a>
+						<b>{{ user.posts_count }}</b>
 						<i>%i18n:mobile.tags.mk-user.posts%</i>
 					</a>
 					<a :href="`${user.username}/following`">
@@ -199,9 +199,6 @@ export default Vue.extend({
 					> i
 						font-size 14px
 
-			> .mk-activity-table
-				margin 12px 0 0 0
-
 		> nav
 			display flex
 			justify-content center

From ebe416aecce8fa94c412970cea641445e30066c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 21:34:49 +0900
Subject: [PATCH 242/286] wip

---
 src/web/app/common/-tags/error.tag            | 215 ------------------
 .../connect-failed.troubleshooter.vue         | 137 +++++++++++
 .../views/components/connect-failed.vue       |  99 ++++++++
 3 files changed, 236 insertions(+), 215 deletions(-)
 delete mode 100644 src/web/app/common/-tags/error.tag
 create mode 100644 src/web/app/common/views/components/connect-failed.troubleshooter.vue
 create mode 100644 src/web/app/common/views/components/connect-failed.vue

diff --git a/src/web/app/common/-tags/error.tag b/src/web/app/common/-tags/error.tag
deleted file mode 100644
index f09c0ce95..000000000
--- a/src/web/app/common/-tags/error.tag
+++ /dev/null
@@ -1,215 +0,0 @@
-<mk-error>
-	<img src="data:image/jpeg;base64,%base64:/assets/error.jpg%" alt=""/>
-	<h1>%i18n:common.tags.mk-error.title%</h1>
-	<p class="text">{
-		'%i18n:common.tags.mk-error.description%'.substr(0, '%i18n:common.tags.mk-error.description%'.indexOf('{'))
-	}<a @click="reload">{
-		'%i18n:common.tags.mk-error.description%'.match(/\{(.+?)\}/)[1]
-	}</a>{
-		'%i18n:common.tags.mk-error.description%'.substr('%i18n:common.tags.mk-error.description%'.indexOf('}') + 1)
-	}</p>
-	<button v-if="!troubleshooting" @click="troubleshoot">%i18n:common.tags.mk-error.troubleshoot%</button>
-	<mk-troubleshooter v-if="troubleshooting"/>
-	<p class="thanks">%i18n:common.tags.mk-error.thanks%</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			width 100%
-			padding 32px 18px
-			text-align center
-
-			> img
-				display block
-				height 200px
-				margin 0 auto
-				pointer-events none
-				user-select none
-
-			> h1
-				display block
-				margin 1.25em auto 0.65em auto
-				font-size 1.5em
-				color #555
-
-			> .text
-				display block
-				margin 0 auto
-				max-width 600px
-				font-size 1em
-				color #666
-
-			> button
-				display block
-				margin 1em auto 0 auto
-				padding 8px 10px
-				color $theme-color-foreground
-				background $theme-color
-
-				&:focus
-					outline solid 3px rgba($theme-color, 0.3)
-
-				&:hover
-					background lighten($theme-color, 10%)
-
-				&:active
-					background darken($theme-color, 10%)
-
-			> mk-troubleshooter
-				margin 1em auto 0 auto
-
-			> .thanks
-				display block
-				margin 2em auto 0 auto
-				padding 2em 0 0 0
-				max-width 600px
-				font-size 0.9em
-				font-style oblique
-				color #aaa
-				border-top solid 1px #eee
-
-			@media (max-width 500px)
-				padding 24px 18px
-				font-size 80%
-
-				> img
-					height 150px
-
-	</style>
-	<script lang="typescript">
-		this.troubleshooting = false;
-
-		this.on('mount', () => {
-			document.title = 'Oops!';
-			document.documentElement.style.background = '#f8f8f8';
-		});
-
-		this.reload = () => {
-			location.reload();
-		};
-
-		this.troubleshoot = () => {
-			this.update({
-				troubleshooting: true
-			});
-		};
-	</script>
-</mk-error>
-
-<mk-troubleshooter>
-	<h1>%fa:wrench%%i18n:common.tags.mk-error.troubleshooter.title%</h1>
-	<div>
-		<p data-wip={ network == null }><template v-if="network != null"><template v-if="network">%fa:check%</template><template v-if="!network">%fa:times%</template></template>{ network == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-network%' : '%i18n:common.tags.mk-error.troubleshooter.network%' }<mk-ellipsis v-if="network == null"/></p>
-		<p v-if="network == true" data-wip={ internet == null }><template v-if="internet != null"><template v-if="internet">%fa:check%</template><template v-if="!internet">%fa:times%</template></template>{ internet == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-internet%' : '%i18n:common.tags.mk-error.troubleshooter.internet%' }<mk-ellipsis v-if="internet == null"/></p>
-		<p v-if="internet == true" data-wip={ server == null }><template v-if="server != null"><template v-if="server">%fa:check%</template><template v-if="!server">%fa:times%</template></template>{ server == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-server%' : '%i18n:common.tags.mk-error.troubleshooter.server%' }<mk-ellipsis v-if="server == null"/></p>
-	</div>
-	<p v-if="!end">%i18n:common.tags.mk-error.troubleshooter.finding%<mk-ellipsis/></p>
-	<p v-if="network === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-network%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-network-desc%</p>
-	<p v-if="internet === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-internet%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-internet-desc%</p>
-	<p v-if="server === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-server%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-server-desc%</p>
-	<p v-if="server === true" class="success"><b>%fa:info-circle%%i18n:common.tags.mk-error.troubleshooter.success%</b><br>%i18n:common.tags.mk-error.troubleshooter.success-desc%</p>
-
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			width 100%
-			max-width 500px
-			text-align left
-			background #fff
-			border-radius 8px
-			border solid 1px #ddd
-
-			> h1
-				margin 0
-				padding 0.6em 1.2em
-				font-size 1em
-				color #444
-				border-bottom solid 1px #eee
-
-				> [data-fa]
-					margin-right 0.25em
-
-			> div
-				overflow hidden
-				padding 0.6em 1.2em
-
-				> p
-					margin 0.5em 0
-					font-size 0.9em
-					color #444
-
-					&[data-wip]
-						color #888
-
-					> [data-fa]
-						margin-right 0.25em
-
-						&.times
-							color #e03524
-
-						&.check
-							color #84c32f
-
-			> p
-				margin 0
-				padding 0.6em 1.2em
-				font-size 1em
-				color #444
-				border-top solid 1px #eee
-
-				> b
-					> [data-fa]
-						margin-right 0.25em
-
-				&.success
-					> b
-						color #39adad
-
-				&:not(.success)
-					> b
-						color #ad4339
-
-	</style>
-	<script lang="typescript">
-		this.on('mount', () => {
-			this.update({
-				network: navigator.onLine
-			});
-
-			if (!this.network) {
-				this.update({
-					end: true
-				});
-				return;
-			}
-
-			// Check internet connection
-			fetch('https://google.com?rand=' + Math.random(), {
-				mode: 'no-cors'
-			}).then(() => {
-				this.update({
-					internet: true
-				});
-
-				// Check misskey server is available
-				fetch(`${_API_URL_}/meta`).then(() => {
-					this.update({
-						end: true,
-						server: true
-					});
-				})
-				.catch(() => {
-					this.update({
-						end: true,
-						server: false
-					});
-				});
-			})
-			.catch(() => {
-				this.update({
-					end: true,
-					internet: false
-				});
-			});
-		});
-	</script>
-</mk-troubleshooter>
diff --git a/src/web/app/common/views/components/connect-failed.troubleshooter.vue b/src/web/app/common/views/components/connect-failed.troubleshooter.vue
new file mode 100644
index 000000000..49396d158
--- /dev/null
+++ b/src/web/app/common/views/components/connect-failed.troubleshooter.vue
@@ -0,0 +1,137 @@
+<template>
+<div class="troubleshooter">
+	<h1>%fa:wrench%%i18n:common.tags.mk-error.troubleshooter.title%</h1>
+	<div>
+		<p :data-wip="network == null">
+			<template v-if="network != null">
+				<template v-if="network">%fa:check%</template>
+				<template v-if="!network">%fa:times%</template>
+			</template>
+			{{ network == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-network%' : '%i18n:common.tags.mk-error.troubleshooter.network%' }}<mk-ellipsis v-if="network == null"/>
+		</p>
+		<p v-if="network == true" :data-wip="internet == null">
+			<template v-if="internet != null">
+				<template v-if="internet">%fa:check%</template>
+				<template v-if="!internet">%fa:times%</template>
+			</template>
+			{{ internet == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-internet%' : '%i18n:common.tags.mk-error.troubleshooter.internet%' }}<mk-ellipsis v-if="internet == null"/>
+		</p>
+		<p v-if="internet == true" :data-wip="server == null">
+			<template v-if="server != null">
+				<template v-if="server">%fa:check%</template>
+				<template v-if="!server">%fa:times%</template>
+			</template>
+			{{ server == null ? '%i18n:common.tags.mk-error.troubleshooter.checking-server%' : '%i18n:common.tags.mk-error.troubleshooter.server%' }}<mk-ellipsis v-if="server == null"/>
+		</p>
+	</div>
+	<p v-if="!end">%i18n:common.tags.mk-error.troubleshooter.finding%<mk-ellipsis/></p>
+	<p v-if="network === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-network%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-network-desc%</p>
+	<p v-if="internet === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-internet%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-internet-desc%</p>
+	<p v-if="server === false"><b>%fa:exclamation-triangle%%i18n:common.tags.mk-error.troubleshooter.no-server%</b><br>%i18n:common.tags.mk-error.troubleshooter.no-server-desc%</p>
+	<p v-if="server === true" class="success"><b>%fa:info-circle%%i18n:common.tags.mk-error.troubleshooter.success%</b><br>%i18n:common.tags.mk-error.troubleshooter.success-desc%</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { apiUrl } from '../../../config';
+
+export default Vue.extend({
+	data() {
+		return {
+			network: navigator.onLine,
+			end: false,
+			internet: false,
+			server: false
+		};
+	},
+	mounted() {
+		if (!this.network) {
+			this.end = true;
+			return;
+		}
+
+		// Check internet connection
+		fetch('https://google.com?rand=' + Math.random(), {
+			mode: 'no-cors'
+		}).then(() => {
+			this.internet = true;
+
+			// Check misskey server is available
+			fetch(`${apiUrl}/meta`).then(() => {
+				this.end = true;
+				this.server = true;
+			})
+			.catch(() => {
+				this.end = true;
+				this.server = false;
+			});
+		})
+		.catch(() => {
+			this.end = true;
+			this.internet = false;
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.troubleshooter
+	width 100%
+	max-width 500px
+	text-align left
+	background #fff
+	border-radius 8px
+	border solid 1px #ddd
+
+	> h1
+		margin 0
+		padding 0.6em 1.2em
+		font-size 1em
+		color #444
+		border-bottom solid 1px #eee
+
+		> [data-fa]
+			margin-right 0.25em
+
+	> div
+		overflow hidden
+		padding 0.6em 1.2em
+
+		> p
+			margin 0.5em 0
+			font-size 0.9em
+			color #444
+
+			&[data-wip]
+				color #888
+
+			> [data-fa]
+				margin-right 0.25em
+
+				&.times
+					color #e03524
+
+				&.check
+					color #84c32f
+
+	> p
+		margin 0
+		padding 0.6em 1.2em
+		font-size 1em
+		color #444
+		border-top solid 1px #eee
+
+		> b
+			> [data-fa]
+				margin-right 0.25em
+
+		&.success
+			> b
+				color #39adad
+
+		&:not(.success)
+			> b
+				color #ad4339
+
+</style>
diff --git a/src/web/app/common/views/components/connect-failed.vue b/src/web/app/common/views/components/connect-failed.vue
new file mode 100644
index 000000000..4761c6d6e
--- /dev/null
+++ b/src/web/app/common/views/components/connect-failed.vue
@@ -0,0 +1,99 @@
+<template>
+<div class="mk-connect-failed">
+	<img src="data:image/jpeg;base64,%base64:/assets/error.jpg%" alt=""/>
+	<h1>%i18n:common.tags.mk-error.title%</h1>
+	<p class="text">
+		{{ '%i18n:common.tags.mk-error.description%'.substr(0, '%i18n:common.tags.mk-error.description%'.indexOf('{')) }}
+		<a @click="location.reload()">{{ '%i18n:common.tags.mk-error.description%'.match(/\{(.+?)\}/)[1] }}</a>
+		{{ '%i18n:common.tags.mk-error.description%'.substr('%i18n:common.tags.mk-error.description%'.indexOf('}') + 1) }}
+	</p>
+	<button v-if="!troubleshooting" @click="troubleshooting = true">%i18n:common.tags.mk-error.troubleshoot%</button>
+	<x-troubleshooter v-if="troubleshooting"/>
+	<p class="thanks">%i18n:common.tags.mk-error.thanks%</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XTroubleshooter from './connect-failed.troubleshooter.vue';
+
+export default Vue.extend({
+	components: {
+		XTroubleshooter
+	},
+	data() {
+		return {
+			troubleshooting: false
+		};
+	},
+	mounted() {
+		document.title = 'Oops!';
+		document.documentElement.style.background = '#f8f8f8';
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-connect-failed
+	width 100%
+	padding 32px 18px
+	text-align center
+
+	> img
+		display block
+		height 200px
+		margin 0 auto
+		pointer-events none
+		user-select none
+
+	> h1
+		display block
+		margin 1.25em auto 0.65em auto
+		font-size 1.5em
+		color #555
+
+	> .text
+		display block
+		margin 0 auto
+		max-width 600px
+		font-size 1em
+		color #666
+
+	> button
+		display block
+		margin 1em auto 0 auto
+		padding 8px 10px
+		color $theme-color-foreground
+		background $theme-color
+
+		&:focus
+			outline solid 3px rgba($theme-color, 0.3)
+
+		&:hover
+			background lighten($theme-color, 10%)
+
+		&:active
+			background darken($theme-color, 10%)
+
+	> .troubleshooter
+		margin 1em auto 0 auto
+
+	> .thanks
+		display block
+		margin 2em auto 0 auto
+		padding 2em 0 0 0
+		max-width 600px
+		font-size 0.9em
+		font-style oblique
+		color #aaa
+		border-top solid 1px #eee
+
+	@media (max-width 500px)
+		padding 24px 18px
+		font-size 80%
+
+		> img
+			height 150px
+
+</style>
+

From 0f36bbd3d4df91a6bb2630c11eed9931ae85a39c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?=
 <syuilotan@yahoo.co.jp>
Date: Wed, 21 Feb 2018 21:51:35 +0900
Subject: [PATCH 243/286] wip

---
 .../desktop/-tags/home-widgets/access-log.tag |  95 ----------------
 .../views/components/widgets/access-log.vue   | 104 ++++++++++++++++++
 2 files changed, 104 insertions(+), 95 deletions(-)
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/access-log.tag
 create mode 100644 src/web/app/desktop/views/components/widgets/access-log.vue

diff --git a/src/web/app/desktop/-tags/home-widgets/access-log.tag b/src/web/app/desktop/-tags/home-widgets/access-log.tag
deleted file mode 100644
index fea18299e..000000000
--- a/src/web/app/desktop/-tags/home-widgets/access-log.tag
+++ /dev/null
@@ -1,95 +0,0 @@
-<mk-access-log-home-widget>
-	<template v-if="data.design == 0">
-		<p class="title">%fa:server%%i18n:desktop.tags.mk-access-log-home-widget.title%</p>
-	</template>
-	<div ref="log">
-		<p each={ requests }>
-			<span class="ip" style="color:{ fg }; background:{ bg }">{ ip }</span>
-			<span>{ method }</span>
-			<span>{ path }</span>
-		</p>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow hidden
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> .title
-				z-index 1
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> [data-fa]
-					margin-right 4px
-
-			> div
-				max-height 250px
-				overflow auto
-
-				> p
-					margin 0
-					padding 8px
-					font-size 0.8em
-					color #555
-
-					&:nth-child(odd)
-						background rgba(0, 0, 0, 0.025)
-
-					> .ip
-						margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		import seedrandom from 'seedrandom';
-
-		this.data = {
-			design: 0
-		};
-
-		this.mixin('widget');
-
-		this.mixin('requests-stream');
-		this.connection = this.requestsStream.getConnection();
-		this.connectionId = this.requestsStream.use();
-
-		this.requests = [];
-
-		this.on('mount', () => {
-			this.connection.on('request', this.onRequest);
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('request', this.onRequest);
-			this.requestsStream.dispose(this.connectionId);
-		});
-
-		this.onRequest = request => {
-			const random = seedrandom(request.ip);
-			const r = Math.floor(random() * 255);
-			const g = Math.floor(random() * 255);
-			const b = Math.floor(random() * 255);
-			const luma = (0.2126 * r) + (0.7152 * g) + (0.0722 * b); // SMPTE C, Rec. 709 weightings
-			request.bg = `rgb(${r}, ${g}, ${b})`;
-			request.fg = luma >= 165 ? '#000' : '#fff';
-
-			this.requests.push(request);
-			if (this.requests.length > 30) this.requests.shift();
-			this.update();
-
-			this.$refs.log.scrollTop = this.$refs.log.scrollHeight;
-		};
-
-		this.func = () => {
-			if (++this.data.design == 2) this.data.design = 0;
-			this.save();
-		};
-	</script>
-</mk-access-log-home-widget>
diff --git a/src/web/app/desktop/views/components/widgets/access-log.vue b/src/web/app/desktop/views/components/widgets/access-log.vue
new file mode 100644
index 000000000..d9f85e722
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/access-log.vue
@@ -0,0 +1,104 @@
+<template>
+<div class="mkw-access-log">
+	<template v-if="props.design == 0">
+		<p class="title">%fa:server%%i18n:desktop.tags.mk-access-log-home-widget.title%</p>
+	</template>
+	<div ref="log">
+		<p v-for="req in requests">
+			<span class="ip" :style="`color:${ req.fg }; background:${ req.bg }`">{{ req.ip }}</span>
+			<span>{{ req.method }}</span>
+			<span>{{ req.path }}</span>
+		</p>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+import seedrandom from 'seedrandom';
+
+export default define({
+	name: 'broadcast',
+	props: () => ({
+		design: 0
+	})
+}).extend({
+	data() {
+		return {
+			requests: [],
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		this.connection = (this as any).os.streams.requestsStream.getConnection();
+		this.connectionId = (this as any).os.streams.requestsStream.use();
+		this.connection.on('request', this.onRequest);
+	},
+	beforeDestroy() {
+		this.connection.off('request', this.onRequest);
+		(this as any).os.streams.requestsStream.dispose(this.connectionId);
+	},
+	methods: {
+		onRequest(request) {
+			const random = seedrandom(request.ip);
+			const r = Math.floor(random() * 255);
+			const g = Math.floor(random() * 255);
+			const b = Math.floor(random() * 255);
+			const luma = (0.2126 * r) + (0.7152 * g) + (0.0722 * b); // SMPTE C, Rec. 709 weightings
+			request.bg = `rgb(${r}, ${g}, ${b})`;
+			request.fg = luma >= 165 ? '#000' : '#fff';
+
+			this.requests.push(request);
+			if (this.requests.length > 30) this.requests.shift();
+
+			(this.$refs.log as any).scrollTop = (this.$refs.log as any).scrollHeight;
+		},
+		func() {
+			if (this.props.design == 1) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-access-log
+	overflow hidden
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	> .title
+		z-index 1
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> [data-fa]
+			margin-right 4px
+
+	> div
+		max-height 250px
+		overflow auto
+
+		> p
+			margin 0
+			padding 8px
+			font-size 0.8em
+			color #555
+
+			&:nth-child(odd)
+				background rgba(0, 0, 0, 0.025)
+
+			> .ip
+				margin-right 4px
+
+</style>

From 9fde555cc389cd2d68128b25598449b1d3ada74b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 00:07:37 +0900
Subject: [PATCH 244/286] wip

---
 package.json                                  |   1 +
 src/web/app/desktop/views/components/home.vue | 155 +++++++++---------
 src/web/app/desktop/views/components/index.ts |   2 +
 .../components/widgets/server.cpu-memory.vue  |   2 +-
 .../views/components/widgets/server.cpu.vue   |   4 +-
 .../views/components/widgets/server.info.vue  |   4 +-
 .../views/components/widgets/server.pie.vue   |   2 +-
 7 files changed, 87 insertions(+), 83 deletions(-)

diff --git a/package.json b/package.json
index 033f76c30..4521b0ceb 100644
--- a/package.json
+++ b/package.json
@@ -187,6 +187,7 @@
 		"vue-loader": "^14.1.1",
 		"vue-router": "^3.0.1",
 		"vue-template-compiler": "^2.5.13",
+		"vuedraggable": "^2.16.0",
 		"web-push": "3.2.5",
 		"webpack": "3.10.0",
 		"webpack-replace-loader": "^1.3.0",
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 9d2198d9d..9962e0da1 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -31,38 +31,51 @@
 				<button @click="addWidget">追加</button>
 			</div>
 			<div class="trash">
-				<div ref="trash"></div>
+				<x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable>
 				<p>ゴミ箱</p>
 			</div>
 		</div>
 	</div>
 	<div class="main">
-		<div v-for="place in ['left', 'main', 'right']" :class="place" :ref="place" :data-place="place">
-			<template v-if="place != 'main'">
-				<template v-for="widget in widgets[place]">
-					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)" :data-widget-id="widget.id">
-						<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id"/>
-					</div>
-					<template v-else>
-						<component :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :ref="widget.id" @chosen="warp"/>
-					</template>
-				</template>
-			</template>
-			<template v-else>
-				<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="place == 'main' && mode == 'timeline'"/>
-				<mk-mentions @loaded="onTlLoaded" v-if="place == 'main' && mode == 'mentions'"/>
-			</template>
-		</div>
+		<template v-if="customize">
+			<x-draggable v-for="place in ['left', 'right']"
+				:list="widgets[place]"
+				:class="place"
+				:data-place="place"
+				:options="{ group: 'x', animation: 150 }"
+				@sort="onWidgetSort"
+				:key="place"
+			>
+				<div v-for="widget in widgets[place]" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
+					<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id"/>
+				</div>
+			</x-draggable>
+			<div class="main">
+				<mk-timeline ref="tl" @loaded="onTlLoaded"/>
+			</div>
+		</template>
+		<template v-else>
+			<div v-for="place in ['left', 'right']" :class="place">
+				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" @chosen="warp"/>
+			</div>
+			<div class="main">
+				<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
+				<mk-mentions @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
+			</div>
+		</template>
 	</div>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import * as XDraggable from 'vuedraggable';
 import * as uuid from 'uuid';
-import * as Sortable from 'sortablejs';
 
 export default Vue.extend({
+	components: {
+		XDraggable
+	},
 	props: {
 		customize: Boolean,
 		mode: {
@@ -72,50 +85,49 @@ export default Vue.extend({
 	},
 	data() {
 		return {
-			widgetAdderSelected: null
+			widgetAdderSelected: null,
+			trash: [],
+			widgets: {
+				left: [],
+				right: []
+			}
 		};
 	},
 	computed: {
-		home(): any {
-			//#region 互換性のため
-			(this as any).os.i.client_settings.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.client_settings.home;
+		home: {
+			get(): any[] {
+				//#region 互換性のため
+				(this as any).os.i.client_settings.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.client_settings.home;
+			},
+			set(value) {
+				(this as any).os.i.client_settings.home = value;
+			}
 		},
-		leftWidgets(): any {
+		left(): any[] {
 			return this.home.filter(w => w.place == 'left');
 		},
-		rightWidgets(): any {
+		right(): any[] {
 			return this.home.filter(w => w.place == 'right');
-		},
-		widgets(): any {
-			return {
-				left: this.leftWidgets,
-				right: this.rightWidgets,
-			};
-		},
-		leftEl(): Element {
-			return (this.$refs.left as Element[])[0];
-		},
-		rightEl(): Element {
-			return (this.$refs.right as Element[])[0];
 		}
 	},
+	created() {
+		this.widgets.left = this.left;
+		this.widgets.right = this.right;
+		this.$watch('os.i', i => {
+			this.widgets.left = this.left;
+			this.widgets.right = this.right;
+		}, {
+			deep: true
+		});
+	},
 	mounted() {
 		this.$nextTick(() => {
-			if (!this.customize) {
-				if (this.leftEl.children.length == 0) {
-					this.leftEl.parentNode.removeChild(this.leftEl);
-				}
-				if (this.rightEl.children.length == 0) {
-					this.rightEl.parentNode.removeChild(this.rightEl);
-				}
-			}
-
 			if (this.customize) {
 				(this as any).apis.dialog({
 					title: '%fa:info-circle%カスタマイズのヒント',
@@ -127,30 +139,6 @@ export default Vue.extend({
 						text: 'Got it!'
 					}]
 				});
-
-				const sortableOption = {
-					group: 'kyoppie',
-					animation: 150,
-					onMove: evt => {
-						const id = evt.dragged.getAttribute('data-widget-id');
-						this.home.find(w => w.id == id).place = evt.to.getAttribute('data-place');
-					},
-					onSort: () => {
-						this.saveHome();
-					}
-				};
-
-				new Sortable(this.leftEl, sortableOption);
-				new Sortable(this.rightEl, sortableOption);
-				new Sortable(this.$refs.trash, Object.assign({}, sortableOption, {
-					onAdd: evt => {
-						const el = evt.item;
-						const id = el.getAttribute('data-widget-id');
-						el.parentNode.removeChild(el);
-						this.home = this.home.filter(w => w.id != id);
-						this.saveHome();
-					}
-				}));
 			}
 		});
 	},
@@ -161,6 +149,12 @@ export default Vue.extend({
 		onWidgetContextmenu(widgetId) {
 			(this.$refs[widgetId] as any)[0].func();
 		},
+		onWidgetSort() {
+			this.saveHome();
+		},
+		onTrash(evt) {
+			this.saveHome();
+		},
 		addWidget() {
 			const widget = {
 				name: this.widgetAdderSelected,
@@ -169,11 +163,15 @@ export default Vue.extend({
 				data: {}
 			};
 
-			this.home.unshift(widget);
-
+			this.widgets.left.unshift(widget);
 			this.saveHome();
 		},
 		saveHome() {
+			const left = this.widgets.left;
+			const right = this.widgets.right;
+			this.home = 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
 			});
@@ -282,6 +280,7 @@ export default Vue.extend({
 		> .main
 			padding 16px
 			width calc(100% - 275px * 2)
+			order 2
 
 		> *:not(main)
 			width 275px
@@ -292,9 +291,11 @@ export default Vue.extend({
 
 		> .left
 			padding-left 16px
+			order 1
 
 		> .right
 			padding-right 16px
+			order 3
 
 		@media (max-width 1100px)
 			> *:not(main)
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 86606a14a..2b5e863ea 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -36,6 +36,7 @@ import wNotifications from './widgets/notifications.vue';
 import wBroadcast from './widgets/broadcast.vue';
 import wTimemachine from './widgets/timemachine.vue';
 import wProfile from './widgets/profile.vue';
+import wServer from './widgets/server.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-notification', uiNotification);
@@ -73,3 +74,4 @@ Vue.component('mkw-notifications', wNotifications);
 Vue.component('mkw-broadcast', wBroadcast);
 Vue.component('mkw-timemachine', wTimemachine);
 Vue.component('mkw-profile', wProfile);
+Vue.component('mkw-server', wServer);
diff --git a/src/web/app/desktop/views/components/widgets/server.cpu-memory.vue b/src/web/app/desktop/views/components/widgets/server.cpu-memory.vue
index 00b3dc3af..d75a14256 100644
--- a/src/web/app/desktop/views/components/widgets/server.cpu-memory.vue
+++ b/src/web/app/desktop/views/components/widgets/server.cpu-memory.vue
@@ -53,7 +53,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import uuid from 'uuid';
+import * as uuid from 'uuid';
 
 export default Vue.extend({
 	props: ['connection'],
diff --git a/src/web/app/desktop/views/components/widgets/server.cpu.vue b/src/web/app/desktop/views/components/widgets/server.cpu.vue
index 96184d188..596c856da 100644
--- a/src/web/app/desktop/views/components/widgets/server.cpu.vue
+++ b/src/web/app/desktop/views/components/widgets/server.cpu.vue
@@ -3,8 +3,8 @@
 	<x-pie class="pie" :value="usage"/>
 	<div>
 		<p>%fa:microchip%CPU</p>
-		<p>{{ cores }} Cores</p>
-		<p>{{ model }}</p>
+		<p>{{ meta.cpu.cores }} Cores</p>
+		<p>{{ meta.cpu.model }}</p>
 	</div>
 </div>
 </template>
diff --git a/src/web/app/desktop/views/components/widgets/server.info.vue b/src/web/app/desktop/views/components/widgets/server.info.vue
index 870baf149..bed6a1b74 100644
--- a/src/web/app/desktop/views/components/widgets/server.info.vue
+++ b/src/web/app/desktop/views/components/widgets/server.info.vue
@@ -14,8 +14,8 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="info" scoped>
-.uptimes
+<style lang="stylus" scoped>
+.info
 	padding 10px 14px
 
 	> p
diff --git a/src/web/app/desktop/views/components/widgets/server.pie.vue b/src/web/app/desktop/views/components/widgets/server.pie.vue
index 45ca8101b..ce2cff1d0 100644
--- a/src/web/app/desktop/views/components/widgets/server.pie.vue
+++ b/src/web/app/desktop/views/components/widgets/server.pie.vue
@@ -14,7 +14,7 @@
 		fill="none"
 		stroke-width="0.1"
 		:stroke="color"/>
-	<text x="50%" y="50%" dy="0.05" text-anchor="middle">{{ (p * 100).toFixed(0) }}%</text>
+	<text x="50%" y="50%" dy="0.05" text-anchor="middle">{{ (value * 100).toFixed(0) }}%</text>
 </svg>
 </template>
 

From 0c0f822df7857b33f2a290767bf54f9f861e432f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 00:14:20 +0900
Subject: [PATCH 245/286] wip

---
 src/web/app/common/define-widget.ts                  |  1 -
 .../app/desktop/views/components/widgets/server.vue  | 12 ++++++++----
 2 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index 930a7c586..fd13a3395 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -30,7 +30,6 @@ export default function<T extends object>(data: {
 			}
 
 			this.$watch('props', newProps => {
-				console.log(this.id, newProps);
 				(this as any).api('i/update_home', {
 					id: this.id,
 					data: newProps
diff --git a/src/web/app/desktop/views/components/widgets/server.vue b/src/web/app/desktop/views/components/widgets/server.vue
index c08056691..1c0da8422 100644
--- a/src/web/app/desktop/views/components/widgets/server.vue
+++ b/src/web/app/desktop/views/components/widgets/server.vue
@@ -62,14 +62,18 @@ export default define({
 	},
 	methods: {
 		toggle() {
-			if (this.props.design == 5) {
+			if (this.props.view == 5) {
+				this.props.view = 0;
+			} else {
+				this.props.view++;
+			}
+		},
+		func() {
+			if (this.props.design == 2) {
 				this.props.design = 0;
 			} else {
 				this.props.design++;
 			}
-		},
-		func() {
-			this.toggle();
 		}
 	}
 });

From 3e201c21e7863a93b88d2088b19d0f23c627b58e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 00:18:19 +0900
Subject: [PATCH 246/286] wip

---
 src/web/app/desktop/views/components/drive-folder.vue | 2 +-
 src/web/app/desktop/views/components/drive.vue        | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/web/app/desktop/views/components/drive-folder.vue b/src/web/app/desktop/views/components/drive-folder.vue
index bfb134501..efb9df30f 100644
--- a/src/web/app/desktop/views/components/drive-folder.vue
+++ b/src/web/app/desktop/views/components/drive-folder.vue
@@ -196,7 +196,7 @@ export default Vue.extend({
 		},
 
 		newWindow() {
-			this.browser.newWindow(this.folder.id);
+			this.browser.newWindow(this.folder);
 		},
 
 		rename() {
diff --git a/src/web/app/desktop/views/components/drive.vue b/src/web/app/desktop/views/components/drive.vue
index aed31f2a8..e256bc6af 100644
--- a/src/web/app/desktop/views/components/drive.vue
+++ b/src/web/app/desktop/views/components/drive.vue
@@ -375,10 +375,10 @@ export default Vue.extend({
 			}
 		},
 
-		newWindow(folderId) {
+		newWindow(folder) {
 			document.body.appendChild(new MkDriveWindow({
 				propsData: {
-					folder: folderId
+					folder: folder
 				}
 			}).$mount().$el);
 		},

From 24142cd5c65a43f8b73152c12e9fb8d0d40a364b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 00:21:07 +0900
Subject: [PATCH 247/286] wip

---
 src/web/app/desktop/views/components/home.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 9962e0da1..6996584cb 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -191,8 +191,8 @@ export default Vue.extend({
 		padding-top 48px
 		background-image url('/assets/desktop/grid.svg')
 
-		> .main > main > *:not(.maintop)
-			cursor not-allowed
+		> .main > .main
+			cursor not-allowed !important
 
 			> *
 				pointer-events none

From a77ac7e651bde5700583e8cedd944ec5c6e98216 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 01:08:49 +0900
Subject: [PATCH 248/286] wip

---
 .../app/common/views/components/messaging.vue |  3 +--
 .../app/desktop/views/components/activity.vue |  4 ++--
 .../choose-folder-from-drive-window.vue       |  4 ++--
 src/web/app/desktop/views/components/home.vue |  3 ++-
 src/web/app/desktop/views/components/index.ts | 22 ++++++++++++++++++-
 .../views/components/widgets/access-log.vue   |  7 ++++--
 .../views/components/widgets/channel.vue      |  2 +-
 .../views/components/widgets/messaging.vue    | 12 +++++-----
 .../views/components/widgets/post-form.vue    |  2 +-
 .../views/components/widgets/slideshow.vue    |  4 ++--
 .../views/components/widgets/trends.vue       |  6 ++---
 11 files changed, 47 insertions(+), 22 deletions(-)

diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index c1d541894..9f04f8933 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -12,7 +12,6 @@
 					@keydown="onSearchResultKeydown(i)"
 					@click="navigate(user)"
 					tabindex="-1"
-					:key="user.id"
 				>
 					<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=32`" alt=""/>
 					<span class="name">{{ user.name }}</span>
@@ -38,7 +37,7 @@
 						<mk-time :time="message.created_at"/>
 					</header>
 					<div class="body">
-						<p class="text"><span class="me" v-if="isMe(message)">%i18n:common.tags.mk-messaging.you%:</span>{{ text }}</p>
+						<p class="text"><span class="me" v-if="isMe(message)">%i18n:common.tags.mk-messaging.you%:</span>{{ message.text }}</p>
 					</div>
 				</div>
 			</a>
diff --git a/src/web/app/desktop/views/components/activity.vue b/src/web/app/desktop/views/components/activity.vue
index 1b2cc9afd..33b53eb70 100644
--- a/src/web/app/desktop/views/components/activity.vue
+++ b/src/web/app/desktop/views/components/activity.vue
@@ -1,12 +1,12 @@
 <template>
-<div class="mk-activity">
+<div class="mk-activity" :data-melt="design == 2">
 	<template v-if="design == 0">
 		<p class="title">%fa:chart-bar%%i18n:desktop.tags.mk-activity-widget.title%</p>
 		<button @click="toggle" title="%i18n:desktop.tags.mk-activity-widget.toggle%">%fa:sort%</button>
 	</template>
 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<template v-else>
-		<x-calender v-show="view == 0" :data="[].concat(activity)"/>
+		<x-calendar v-show="view == 0" :data="[].concat(activity)"/>
 		<x-chart v-show="view == 1" :data="[].concat(activity)"/>
 	</template>
 </div>
diff --git a/src/web/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/web/app/desktop/views/components/choose-folder-from-drive-window.vue
index 0e598937e..8111ffcf0 100644
--- a/src/web/app/desktop/views/components/choose-folder-from-drive-window.vue
+++ b/src/web/app/desktop/views/components/choose-folder-from-drive-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" is-modal width='800px' height='500px' @closed="$destroy">
+<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy">
 	<span slot="header">
 		<span v-html="title" :class="$style.title"></span>
 	</span>
@@ -10,7 +10,7 @@
 		:multiple="false"
 	/>
 	<div :class="$style.footer">
-		<button :class="$style.cancel" @click="close">キャンセル</button>
+		<button :class="$style.cancel" @click="cancel">キャンセル</button>
 		<button :class="$style.ok" @click="ok">決定</button>
 	</div>
 </mk-window>
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 6996584cb..1191ad895 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -147,7 +147,8 @@ export default Vue.extend({
 			this.$emit('loaded');
 		},
 		onWidgetContextmenu(widgetId) {
-			(this.$refs[widgetId] as any)[0].func();
+			const w = (this.$refs[widgetId] as any)[0];
+			if (w.func) w.func();
 		},
 		onWidgetSort() {
 			this.saveHome();
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 2b5e863ea..3bcfc2fdd 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -37,6 +37,16 @@ import wBroadcast from './widgets/broadcast.vue';
 import wTimemachine from './widgets/timemachine.vue';
 import wProfile from './widgets/profile.vue';
 import wServer from './widgets/server.vue';
+import wActivity from './widgets/activity.vue';
+import wRss from './widgets/rss.vue';
+import wTrends from './widgets/trends.vue';
+import wVersion from './widgets/version.vue';
+import wUsers from './widgets/users.vue';
+import wPolls from './widgets/polls.vue';
+import wPostForm from './widgets/post-form.vue';
+import wMessaging from './widgets/messaging.vue';
+import wChannel from './widgets/channel.vue';
+import wAccessLog from './widgets/access-log.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-notification', uiNotification);
@@ -67,7 +77,7 @@ Vue.component('mk-activity', activity);
 Vue.component('mkw-nav', wNav);
 Vue.component('mkw-calendar', wCalendar);
 Vue.component('mkw-photo-stream', wPhotoStream);
-Vue.component('mkw-slideshoe', wSlideshow);
+Vue.component('mkw-slideshow', wSlideshow);
 Vue.component('mkw-tips', wTips);
 Vue.component('mkw-donation', wDonation);
 Vue.component('mkw-notifications', wNotifications);
@@ -75,3 +85,13 @@ Vue.component('mkw-broadcast', wBroadcast);
 Vue.component('mkw-timemachine', wTimemachine);
 Vue.component('mkw-profile', wProfile);
 Vue.component('mkw-server', wServer);
+Vue.component('mkw-activity', wActivity);
+Vue.component('mkw-rss', wRss);
+Vue.component('mkw-trends', wTrends);
+Vue.component('mkw-version', wVersion);
+Vue.component('mkw-users', wUsers);
+Vue.component('mkw-polls', wPolls);
+Vue.component('mkw-post-form', wPostForm);
+Vue.component('mkw-messaging', wMessaging);
+Vue.component('mkw-channel', wChannel);
+Vue.component('mkw-access-log', wAccessLog);
diff --git a/src/web/app/desktop/views/components/widgets/access-log.vue b/src/web/app/desktop/views/components/widgets/access-log.vue
index d9f85e722..ad0a22829 100644
--- a/src/web/app/desktop/views/components/widgets/access-log.vue
+++ b/src/web/app/desktop/views/components/widgets/access-log.vue
@@ -6,7 +6,7 @@
 	<div ref="log">
 		<p v-for="req in requests">
 			<span class="ip" :style="`color:${ req.fg }; background:${ req.bg }`">{{ req.ip }}</span>
-			<span>{{ req.method }}</span>
+			<b>{{ req.method }}</b>
 			<span>{{ req.path }}</span>
 		</p>
 	</div>
@@ -15,7 +15,7 @@
 
 <script lang="ts">
 import define from '../../../../common/define-widget';
-import seedrandom from 'seedrandom';
+import * as seedrandom from 'seedrandom';
 
 export default define({
 	name: 'broadcast',
@@ -101,4 +101,7 @@ export default define({
 			> .ip
 				margin-right 4px
 
+			> b
+				margin-right 4px
+
 </style>
diff --git a/src/web/app/desktop/views/components/widgets/channel.vue b/src/web/app/desktop/views/components/widgets/channel.vue
index 484dca9f6..1b98be734 100644
--- a/src/web/app/desktop/views/components/widgets/channel.vue
+++ b/src/web/app/desktop/views/components/widgets/channel.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mkw-channel">
-	<template v-if="!data.compact">
+	<template v-if="!props.compact">
 		<p class="title">%fa:tv%{{ channel ? channel.title : '%i18n:desktop.tags.mk-channel-home-widget.title%' }}</p>
 		<button @click="settings" title="%i18n:desktop.tags.mk-channel-home-widget.settings%">%fa:cog%</button>
 	</template>
diff --git a/src/web/app/desktop/views/components/widgets/messaging.vue b/src/web/app/desktop/views/components/widgets/messaging.vue
index 039a524f5..e510a07dc 100644
--- a/src/web/app/desktop/views/components/widgets/messaging.vue
+++ b/src/web/app/desktop/views/components/widgets/messaging.vue
@@ -7,6 +7,8 @@
 
 <script lang="ts">
 import define from '../../../../common/define-widget';
+import MkMessagingRoomWindow from '../messaging-room-window.vue';
+
 export default define({
 	name: 'messaging',
 	props: () => ({
@@ -15,11 +17,11 @@ export default define({
 }).extend({
 	methods: {
 		navigate(user) {
-			if (this.platform == 'desktop') {
-				this.wapi_openMessagingRoomWindow(user);
-			} else {
-				// TODO: open room page in new tab
-			}
+			document.body.appendChild(new MkMessagingRoomWindow({
+				propsData: {
+					user: user
+				}
+			}).$mount().$el);
 		},
 		func() {
 			if (this.props.design == 1) {
diff --git a/src/web/app/desktop/views/components/widgets/post-form.vue b/src/web/app/desktop/views/components/widgets/post-form.vue
index 94b03f84a..ab87ba721 100644
--- a/src/web/app/desktop/views/components/widgets/post-form.vue
+++ b/src/web/app/desktop/views/components/widgets/post-form.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mkw-post-form">
-	<template v-if="data.design == 0">
+	<template v-if="props.design == 0">
 		<p class="title">%fa:pencil-alt%%i18n:desktop.tags.mk-post-form-home-widget.title%</p>
 	</template>
 	<textarea :disabled="posting" v-model="text" @keydown="onKeydown" placeholder="%i18n:desktop.tags.mk-post-form-home-widget.placeholder%"></textarea>
diff --git a/src/web/app/desktop/views/components/widgets/slideshow.vue b/src/web/app/desktop/views/components/widgets/slideshow.vue
index 75af3c0f1..c2f4eb70d 100644
--- a/src/web/app/desktop/views/components/widgets/slideshow.vue
+++ b/src/web/app/desktop/views/components/widgets/slideshow.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="mkw-slideshow">
 	<div @click="choose">
-		<p v-if="data.folder === undefined">クリックしてフォルダを指定してください</p>
-		<p v-if="data.folder !== undefined && images.length == 0 && !fetching">このフォルダには画像がありません</p>
+		<p v-if="props.folder === undefined">クリックしてフォルダを指定してください</p>
+		<p v-if="props.folder !== undefined && images.length == 0 && !fetching">このフォルダには画像がありません</p>
 		<div ref="slideA" class="slide a"></div>
 		<div ref="slideB" class="slide b"></div>
 	</div>
diff --git a/src/web/app/desktop/views/components/widgets/trends.vue b/src/web/app/desktop/views/components/widgets/trends.vue
index a764639ce..934351b8a 100644
--- a/src/web/app/desktop/views/components/widgets/trends.vue
+++ b/src/web/app/desktop/views/components/widgets/trends.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="mkw-trends">
-	<template v-if="!data.compact">
+	<template v-if="!props.compact">
 		<p class="title">%fa:fire%%i18n:desktop.tags.mk-trends-home-widget.title%</p>
 		<button @click="fetch" title="%i18n:desktop.tags.mk-trends-home-widget.refresh%">%fa:sync%</button>
 	</template>
 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<div class="post" v-else-if="post != null">
-		<p class="text"><a href="/{ post.user.username }/{ post.id }">{ post.text }</a></p>
-		<p class="author">―<a href="/{ post.user.username }">@{ post.user.username }</a></p>
+		<p class="text"><router-link :to="`/${ post.user.username }/${ post.id }`">{{ post.text }}</router-link></p>
+		<p class="author">―<router-link :to="`/${ post.user.username }`">@{{ post.user.username }}</router-link></p>
 	</div>
 	<p class="empty" v-else>%i18n:desktop.tags.mk-trends-home-widget.nothing%</p>
 </div>

From 3d4fccef457babe88c0bb4e6b470c30e03ae266e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 01:25:57 +0900
Subject: [PATCH 249/286] wip

---
 src/web/app/common/views/components/index.ts  |  2 +
 .../app/desktop/scripts/password-dialog.ts    | 11 -----
 .../views/components/password-setting.vue     | 37 ---------------
 .../{2fa-setting.vue => settings.2fa.vue}     | 28 ++++++-----
 .../{mute-setting.vue => settings.mute.vue}   |  4 +-
 .../views/components/settings.password.vue    | 47 +++++++++++++++++++
 .../app/desktop/views/components/settings.vue | 14 ++++--
 .../views/components/widgets/access-log.vue   |  1 +
 8 files changed, 78 insertions(+), 66 deletions(-)
 delete mode 100644 src/web/app/desktop/scripts/password-dialog.ts
 delete mode 100644 src/web/app/desktop/views/components/password-setting.vue
 rename src/web/app/desktop/views/components/{2fa-setting.vue => settings.2fa.vue} (68%)
 rename src/web/app/desktop/views/components/{mute-setting.vue => settings.mute.vue} (92%)
 create mode 100644 src/web/app/desktop/views/components/settings.password.vue

diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index a61022dbe..bde313910 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -17,6 +17,7 @@ import ellipsis from './ellipsis.vue';
 import messaging from './messaging.vue';
 import messagingRoom from './messaging-room.vue';
 import urlPreview from './url-preview.vue';
+import twitterSetting from './twitter-setting.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
@@ -35,3 +36,4 @@ Vue.component('mk-ellipsis', ellipsis);
 Vue.component('mk-messaging', messaging);
 Vue.component('mk-messaging-room', messagingRoom);
 Vue.component('mk-url-preview', urlPreview);
+Vue.component('mk-twitter-setting', twitterSetting);
diff --git a/src/web/app/desktop/scripts/password-dialog.ts b/src/web/app/desktop/scripts/password-dialog.ts
deleted file mode 100644
index 39d7f3db7..000000000
--- a/src/web/app/desktop/scripts/password-dialog.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import * as riot from 'riot';
-
-export default (title, onOk, onCancel) => {
-	const dialog = document.body.appendChild(document.createElement('mk-input-dialog'));
-	return (riot as any).mount(dialog, {
-		title: title,
-		type: 'password',
-		onOk: onOk,
-		onCancel: onCancel
-	});
-};
diff --git a/src/web/app/desktop/views/components/password-setting.vue b/src/web/app/desktop/views/components/password-setting.vue
deleted file mode 100644
index 883a494cc..000000000
--- a/src/web/app/desktop/views/components/password-setting.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-<template>
-<div>
-	<button @click="reset" class="ui primary">%i18n:desktop.tags.mk-password-setting.reset%</button>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import passwordDialog from '../../scripts/password-dialog';
-import dialog from '../../scripts/dialog';
-import notify from '../../scripts/notify';
-
-export default Vue.extend({
-	methods: {
-		reset() {
-			passwordDialog('%i18n:desktop.tags.mk-password-setting.enter-current-password%', currentPassword => {
-				passwordDialog('%i18n:desktop.tags.mk-password-setting.enter-new-password%', newPassword => {
-					passwordDialog('%i18n:desktop.tags.mk-password-setting.enter-new-password-again%', newPassword2 => {
-						if (newPassword !== newPassword2) {
-							dialog(null, '%i18n:desktop.tags.mk-password-setting.not-match%', [{
-								text: 'OK'
-							}]);
-							return;
-						}
-						(this as any).api('i/change_password', {
-							current_password: currentPassword,
-							new_password: newPassword
-						}).then(() => {
-							notify('%i18n:desktop.tags.mk-password-setting.changed%');
-						});
-					});
-				});
-			});
-		}
-	}
-});
-</script>
diff --git a/src/web/app/desktop/views/components/2fa-setting.vue b/src/web/app/desktop/views/components/settings.2fa.vue
similarity index 68%
rename from src/web/app/desktop/views/components/2fa-setting.vue
rename to src/web/app/desktop/views/components/settings.2fa.vue
index 8271cbbf3..87783e799 100644
--- a/src/web/app/desktop/views/components/2fa-setting.vue
+++ b/src/web/app/desktop/views/components/settings.2fa.vue
@@ -1,16 +1,16 @@
 <template>
-<div class="mk-2fa-setting">
+<div class="2fa">
 	<p>%i18n:desktop.tags.mk-2fa-setting.intro%<a href="%i18n:desktop.tags.mk-2fa-setting.url%" target="_blank">%i18n:desktop.tags.mk-2fa-setting.detail%</a></p>
 	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-2fa-setting.caution%</p></div>
-	<p v-if="!data && !I.two_factor_enabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
-	<template v-if="I.two_factor_enabled">
+	<p v-if="!data && !os.i.two_factor_enabled"><button @click="register" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
+	<template v-if="os.i.two_factor_enabled">
 		<p>%i18n:desktop.tags.mk-2fa-setting.already-registered%</p>
 		<button @click="unregister" class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button>
 	</template>
 	<div v-if="data">
 		<ol>
 			<li>%i18n:desktop.tags.mk-2fa-setting.authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:desktop.tags.mk-2fa-setting.howtoinstall%</a></li>
-			<li>%i18n:desktop.tags.mk-2fa-setting.scan%<br><img src={ data.qr }></li>
+			<li>%i18n:desktop.tags.mk-2fa-setting.scan%<br><img :src="data.qr"></li>
 			<li>%i18n:desktop.tags.mk-2fa-setting.done%<br>
 				<input type="number" v-model="token" class="ui">
 				<button @click="submit" class="ui primary">%i18n:desktop.tags.mk-2fa-setting.submit%</button>
@@ -23,8 +23,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import passwordDialog from '../../scripts/password-dialog';
-import notify from '../../scripts/notify';
 
 export default Vue.extend({
 	data() {
@@ -35,7 +33,10 @@ export default Vue.extend({
 	},
 	methods: {
 		register() {
-			passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => {
+			(this as any).apis.input({
+				title: '%i18n:desktop.tags.mk-2fa-setting.enter-password%',
+				type: 'password'
+			}).then(password => {
 				(this as any).api('i/2fa/register', {
 					password: password
 				}).then(data => {
@@ -45,11 +46,14 @@ export default Vue.extend({
 		},
 
 		unregister() {
-			passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => {
+			(this as any).apis.input({
+				title: '%i18n:desktop.tags.mk-2fa-setting.enter-password%',
+				type: 'password'
+			}).then(password => {
 				(this as any).api('i/2fa/unregister', {
 					password: password
 				}).then(() => {
-					notify('%i18n:desktop.tags.mk-2fa-setting.unregistered%');
+					(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.unregistered%');
 					(this as any).os.i.two_factor_enabled = false;
 				});
 			});
@@ -59,10 +63,10 @@ export default Vue.extend({
 			(this as any).api('i/2fa/done', {
 				token: this.token
 			}).then(() => {
-				notify('%i18n:desktop.tags.mk-2fa-setting.success%');
+				(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.success%');
 				(this as any).os.i.two_factor_enabled = true;
 			}).catch(() => {
-				notify('%i18n:desktop.tags.mk-2fa-setting.failed%');
+				(this as any).apis.notify('%i18n:desktop.tags.mk-2fa-setting.failed%');
 			});
 		}
 	}
@@ -70,7 +74,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-2fa-setting
+.2fa
 	color #4a535a
 
 </style>
diff --git a/src/web/app/desktop/views/components/mute-setting.vue b/src/web/app/desktop/views/components/settings.mute.vue
similarity index 92%
rename from src/web/app/desktop/views/components/mute-setting.vue
rename to src/web/app/desktop/views/components/settings.mute.vue
index fe78401af..0768b54ef 100644
--- a/src/web/app/desktop/views/components/mute-setting.vue
+++ b/src/web/app/desktop/views/components/settings.mute.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-mute-setting">
+<div>
 	<div class="none ui info" v-if="!fetching && users.length == 0">
 		<p>%fa:info-circle%%i18n:desktop.tags.mk-mute-setting.no-users%</p>
 	</div>
@@ -18,7 +18,7 @@ export default Vue.extend({
 	data() {
 		return {
 			fetching: true,
-			users: null
+			users: []
 		};
 	},
 	mounted() {
diff --git a/src/web/app/desktop/views/components/settings.password.vue b/src/web/app/desktop/views/components/settings.password.vue
new file mode 100644
index 000000000..be3f0370d
--- /dev/null
+++ b/src/web/app/desktop/views/components/settings.password.vue
@@ -0,0 +1,47 @@
+<template>
+<div>
+	<button @click="reset" class="ui primary">%i18n:desktop.tags.mk-password-setting.reset%</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	methods: {
+		reset() {
+			(this as any).apis.input({
+				title: '%i18n:desktop.tags.mk-password-setting.enter-current-password%',
+				type: 'password'
+			}).then(currentPassword => {
+				(this as any).apis.input({
+					title: '%i18n:desktop.tags.mk-password-setting.enter-new-password%',
+					type: 'password'
+				}).then(newPassword => {
+					(this as any).apis.input({
+						title: '%i18n:desktop.tags.mk-password-setting.enter-new-password-again%',
+						type: 'password'
+					}).then(newPassword2 => {
+						if (newPassword !== newPassword2) {
+							(this as any).apis.dialog({
+								title: null,
+								text: '%i18n:desktop.tags.mk-password-setting.not-match%',
+								actions: [{
+									text: 'OK'
+								}]
+							});
+							return;
+						}
+						(this as any).api('i/change_password', {
+							current_password: currentPassword,
+							new_password: newPassword
+						}).then(() => {
+							(this as any).apis.notify('%i18n:desktop.tags.mk-password-setting.changed%');
+						});
+					});
+				});
+			});
+		}
+	}
+});
+</script>
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 148e11ed2..b36698b64 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -30,7 +30,7 @@
 
 		<section class="mute" v-show="page == 'mute'">
 			<h1>%i18n:desktop.tags.mk-settings.mute%</h1>
-			<mk-mute-setting/>
+			<x-mute/>
 		</section>
 
 		<section class="apps" v-show="page == 'apps'">
@@ -45,12 +45,12 @@
 
 		<section class="password" v-show="page == 'security'">
 			<h1>%i18n:desktop.tags.mk-settings.password%</h1>
-			<mk-password-setting/>
+			<x-password/>
 		</section>
 
 		<section class="2fa" v-show="page == 'security'">
 			<h1>%i18n:desktop.tags.mk-settings.2fa%</h1>
-			<mk-2fa-setting/>
+			<x-2fa/>
 		</section>
 
 		<section class="signin" v-show="page == 'security'">
@@ -74,10 +74,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import XProfile from './settings.profile.vue';
+import XMute from './settings.mute.vue';
+import XPassword from './settings.password.vue';
+import X2fa from './settings.2fa.vue';
 
 export default Vue.extend({
 	components: {
-		XProfile
+		XProfile,
+		XMute,
+		XPassword,
+		X2fa
 	},
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/access-log.vue b/src/web/app/desktop/views/components/widgets/access-log.vue
index ad0a22829..a04da1daa 100644
--- a/src/web/app/desktop/views/components/widgets/access-log.vue
+++ b/src/web/app/desktop/views/components/widgets/access-log.vue
@@ -100,6 +100,7 @@ export default define({
 
 			> .ip
 				margin-right 4px
+				padding 0 4px
 
 			> b
 				margin-right 4px

From abf1c30ce6198545c45e88c10f0922912ca5ac84 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 02:00:30 +0900
Subject: [PATCH 250/286] wip

---
 src/web/app/desktop/script.ts                 | 23 +++++------
 src/web/app/mobile/api/choose-drive-file.ts   | 18 +++++++++
 src/web/app/mobile/api/choose-drive-folder.ts | 17 ++++++++
 src/web/app/mobile/api/dialog.ts              |  5 +++
 src/web/app/mobile/api/input.ts               |  5 +++
 src/web/app/mobile/api/post.ts                | 14 +++++++
 src/web/app/mobile/script.ts                  | 40 +++++++++++++++++--
 .../views/components/drive-folder-chooser.vue |  2 +-
 webpack/webpack.config.ts                     |  2 +-
 9 files changed, 108 insertions(+), 18 deletions(-)
 create mode 100644 src/web/app/mobile/api/choose-drive-file.ts
 create mode 100644 src/web/app/mobile/api/choose-drive-folder.ts
 create mode 100644 src/web/app/mobile/api/dialog.ts
 create mode 100644 src/web/app/mobile/api/input.ts
 create mode 100644 src/web/app/mobile/api/post.ts

diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index 4f2ac61ee..3c560033f 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -35,6 +35,7 @@ init(async (launch) => {
 	// Register components
 	require('./views/components');
 
+	// Launch the app
 	const [app, os] = launch(os => ({
 		chooseDriveFolder,
 		chooseDriveFile,
@@ -65,19 +66,15 @@ init(async (launch) => {
 		}
 	}
 
-	app.$router.addRoutes([{
-		path: '/', name: 'index', component: MkIndex
-	}, {
-		path: '/i/customize-home', component: MkHomeCustomize
-	}, {
-		path: '/i/drive', component: MkDrive
-	}, {
-		path: '/i/drive/folder/:folder', component: MkDrive
-	}, {
-		path: '/selectdrive', component: MkSelectDrive
-	}, {
-		path: '/:user', component: MkUser
-	}]);
+	// Routing
+	app.$router.addRoutes([
+		{ path: '/', name: 'index', component: MkIndex },
+		{ path: '/i/customize-home', component: MkHomeCustomize },
+		{ path: '/i/drive', component: MkDrive },
+		{ path: '/i/drive/folder/:folder', component: MkDrive },
+		{ path: '/selectdrive', component: MkSelectDrive },
+		{ path: '/:user', component: MkUser }
+	]);
 }, true);
 
 function registerNotifications(stream: HomeStreamManager) {
diff --git a/src/web/app/mobile/api/choose-drive-file.ts b/src/web/app/mobile/api/choose-drive-file.ts
new file mode 100644
index 000000000..b1a78f236
--- /dev/null
+++ b/src/web/app/mobile/api/choose-drive-file.ts
@@ -0,0 +1,18 @@
+import Chooser from '../views/components/drive-file-chooser.vue';
+
+export default function(opts) {
+	return new Promise((res, rej) => {
+		const o = opts || {};
+		const w = new Chooser({
+			propsData: {
+				title: o.title,
+				multiple: o.multiple,
+				initFolder: o.currentFolder
+			}
+		}).$mount();
+		w.$once('selected', file => {
+			res(file);
+		});
+		document.body.appendChild(w.$el);
+	});
+}
diff --git a/src/web/app/mobile/api/choose-drive-folder.ts b/src/web/app/mobile/api/choose-drive-folder.ts
new file mode 100644
index 000000000..d1f97d148
--- /dev/null
+++ b/src/web/app/mobile/api/choose-drive-folder.ts
@@ -0,0 +1,17 @@
+import Chooser from '../views/components/drive-folder-chooser.vue';
+
+export default function(opts) {
+	return new Promise((res, rej) => {
+		const o = opts || {};
+		const w = new Chooser({
+			propsData: {
+				title: o.title,
+				initFolder: o.currentFolder
+			}
+		}).$mount();
+		w.$once('selected', folder => {
+			res(folder);
+		});
+		document.body.appendChild(w.$el);
+	});
+}
diff --git a/src/web/app/mobile/api/dialog.ts b/src/web/app/mobile/api/dialog.ts
new file mode 100644
index 000000000..a2378767b
--- /dev/null
+++ b/src/web/app/mobile/api/dialog.ts
@@ -0,0 +1,5 @@
+export default function(opts) {
+	return new Promise<string>((res, rej) => {
+		alert('dialog not implemented yet');
+	});
+}
diff --git a/src/web/app/mobile/api/input.ts b/src/web/app/mobile/api/input.ts
new file mode 100644
index 000000000..fcff68cfb
--- /dev/null
+++ b/src/web/app/mobile/api/input.ts
@@ -0,0 +1,5 @@
+export default function(opts) {
+	return new Promise<string>((res, rej) => {
+		alert('input not implemented yet');
+	});
+}
diff --git a/src/web/app/mobile/api/post.ts b/src/web/app/mobile/api/post.ts
new file mode 100644
index 000000000..11ffc779f
--- /dev/null
+++ b/src/web/app/mobile/api/post.ts
@@ -0,0 +1,14 @@
+
+export default opts => {
+	const app = document.getElementById('app');
+	app.style.display = 'none';
+
+	function recover() {
+		app.style.display = 'block';
+	}
+
+	const form = riot.mount(document.body.appendChild(document.createElement('mk-post-form')), opts)[0];
+	form
+		.on('cancel', recover)
+		.on('post', recover);
+};
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index f2d617f3a..339c9a8e4 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -5,9 +5,22 @@
 // Style
 import './style.styl';
 
-require('./tags');
 import init from '../init';
 
+import chooseDriveFolder from './api/choose-drive-folder';
+import chooseDriveFile from './api/choose-drive-file';
+import dialog from './api/dialog';
+import input from './api/input';
+import post from './api/post';
+import notify from './api/notify';
+import updateAvatar from './api/update-avatar';
+import updateBanner from './api/update-banner';
+
+import MkIndex from './views/pages/index.vue';
+import MkUser from './views/pages/user/user.vue';
+import MkSelectDrive from './views/pages/selectdrive.vue';
+import MkDrive from './views/pages/drive.vue';
+
 /**
  * init
  */
@@ -15,9 +28,30 @@ init((launch) => {
 	// Register directives
 	require('./views/directives');
 
+	// Register components
+	require('./views/components');
+
 	// http://qiita.com/junya/items/3ff380878f26ca447f85
 	document.body.setAttribute('ontouchstart', '');
 
-	// Start routing
-	//route(mios);
+	// Launch the app
+	const [app, os] = launch(os => ({
+		chooseDriveFolder,
+		chooseDriveFile,
+		dialog,
+		input,
+		post,
+		notify,
+		updateAvatar: updateAvatar(os),
+		updateBanner: updateBanner(os)
+	}));
+
+	// Routing
+	app.$router.addRoutes([
+		{ path: '/', name: 'index', component: MkIndex },
+		{ path: '/i/drive', component: MkDrive },
+		{ path: '/i/drive/folder/:folder', component: MkDrive },
+		{ path: '/selectdrive', component: MkSelectDrive },
+		{ path: '/:user', component: MkUser }
+	]);
 }, true);
diff --git a/src/web/app/mobile/views/components/drive-folder-chooser.vue b/src/web/app/mobile/views/components/drive-folder-chooser.vue
index 53cc67c6c..853078664 100644
--- a/src/web/app/mobile/views/components/drive-folder-chooser.vue
+++ b/src/web/app/mobile/views/components/drive-folder-chooser.vue
@@ -4,7 +4,7 @@
 		<header>
 			<h1>%i18n:mobile.tags.mk-drive-folder-selector.select-folder%</h1>
 			<button class="close" @click="cancel">%fa:times%</button>
-			<button v-if="opts.multiple" class="ok" @click="ok">%fa:check%</button>
+			<button class="ok" @click="ok">%fa:check%</button>
 		</header>
 		<mk-drive ref="browser"
 			select-folder
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 3686d0b65..bd8c6d120 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -29,7 +29,7 @@ module.exports = Object.keys(langs).map(lang => {
 	// Entries
 	const entry = {
 		desktop: './src/web/app/desktop/script.ts',
-		//mobile: './src/web/app/mobile/script.ts',
+		mobile: './src/web/app/mobile/script.ts',
 		//ch: './src/web/app/ch/script.ts',
 		//stats: './src/web/app/stats/script.ts',
 		//status: './src/web/app/status/script.ts',

From 73029df58ae7c32ad9cc1e3ff4828e0547635bf3 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 02:15:46 +0900
Subject: [PATCH 251/286] wip

---
 src/web/app/common/mios.ts                    |  5 ++-
 src/web/app/desktop/api/post.ts               | 21 +++++++++++--
 src/web/app/mobile/api/notify.ts              |  3 ++
 src/web/app/mobile/api/post.ts                | 31 +++++++++++++++----
 src/web/app/mobile/script.ts                  |  8 ++---
 .../app/mobile/views/components/post-form.vue |  2 +-
 6 files changed, 53 insertions(+), 17 deletions(-)
 create mode 100644 src/web/app/mobile/api/notify.ts

diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index 4b9375f54..a37c5d6f7 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -42,7 +42,10 @@ export type API = {
 		default?: string;
 	}) => Promise<string>;
 
-	post: () => void;
+	post: (opts?: {
+		reply?: any;
+		repost?: any;
+	}) => void;
 
 	notify: (message: string) => void;
 };
diff --git a/src/web/app/desktop/api/post.ts b/src/web/app/desktop/api/post.ts
index 4eebd747f..cf49615df 100644
--- a/src/web/app/desktop/api/post.ts
+++ b/src/web/app/desktop/api/post.ts
@@ -1,6 +1,21 @@
 import PostFormWindow from '../views/components/post-form-window.vue';
+import RepostFormWindow from '../views/components/repost-form-window.vue';
 
-export default function() {
-	const vm = new PostFormWindow().$mount();
-	document.body.appendChild(vm.$el);
+export default function(opts) {
+	const o = opts || {};
+	if (o.repost) {
+		const vm = new RepostFormWindow({
+			propsData: {
+				repost: o.repost
+			}
+		}).$mount();
+		document.body.appendChild(vm.$el);
+	} else {
+		const vm = new PostFormWindow({
+			propsData: {
+				reply: o.reply
+			}
+		}).$mount();
+		document.body.appendChild(vm.$el);
+	}
 }
diff --git a/src/web/app/mobile/api/notify.ts b/src/web/app/mobile/api/notify.ts
new file mode 100644
index 000000000..82780d196
--- /dev/null
+++ b/src/web/app/mobile/api/notify.ts
@@ -0,0 +1,3 @@
+export default function(message) {
+	alert(message);
+}
diff --git a/src/web/app/mobile/api/post.ts b/src/web/app/mobile/api/post.ts
index 11ffc779f..3ceb10496 100644
--- a/src/web/app/mobile/api/post.ts
+++ b/src/web/app/mobile/api/post.ts
@@ -1,5 +1,9 @@
+import PostForm from '../views/components/post-form.vue';
+import RepostForm from '../views/components/repost-form.vue';
+
+export default function(opts) {
+	const o = opts || {};
 
-export default opts => {
 	const app = document.getElementById('app');
 	app.style.display = 'none';
 
@@ -7,8 +11,23 @@ export default opts => {
 		app.style.display = 'block';
 	}
 
-	const form = riot.mount(document.body.appendChild(document.createElement('mk-post-form')), opts)[0];
-	form
-		.on('cancel', recover)
-		.on('post', recover);
-};
+	if (o.repost) {
+		const vm = new RepostForm({
+			propsData: {
+				repost: o.repost
+			}
+		}).$mount();
+		vm.$once('cancel', recover);
+		vm.$once('post', recover);
+		document.body.appendChild(vm.$el);
+	} else {
+		const vm = new PostForm({
+			propsData: {
+				reply: o.reply
+			}
+		}).$mount();
+		vm.$once('cancel', recover);
+		vm.$once('post', recover);
+		document.body.appendChild(vm.$el);
+	}
+}
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index 339c9a8e4..1d25280d9 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -13,8 +13,6 @@ import dialog from './api/dialog';
 import input from './api/input';
 import post from './api/post';
 import notify from './api/notify';
-import updateAvatar from './api/update-avatar';
-import updateBanner from './api/update-banner';
 
 import MkIndex from './views/pages/index.vue';
 import MkUser from './views/pages/user/user.vue';
@@ -35,15 +33,13 @@ init((launch) => {
 	document.body.setAttribute('ontouchstart', '');
 
 	// Launch the app
-	const [app, os] = launch(os => ({
+	const [app] = launch(os => ({
 		chooseDriveFolder,
 		chooseDriveFile,
 		dialog,
 		input,
 		post,
-		notify,
-		updateAvatar: updateAvatar(os),
-		updateBanner: updateBanner(os)
+		notify
 	}));
 
 	// Routing
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index 091056bcd..6c41a73b5 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -25,7 +25,7 @@
 		<button class="poll" @click="addPoll">%fa:chart-pie%</button>
 		<input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
 	</div>
-</div
+</div>
 </template>
 
 <script lang="ts">

From 0c092b8050a099d1ada717102222aebe6f53b8d8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 02:22:10 +0900
Subject: [PATCH 252/286] wip

---
 src/web/app/mobile/api/post.ts | 32 ++++++++++++++++++++------------
 src/web/app/mobile/script.ts   |  2 +-
 2 files changed, 21 insertions(+), 13 deletions(-)

diff --git a/src/web/app/mobile/api/post.ts b/src/web/app/mobile/api/post.ts
index 3ceb10496..3b14e0c1d 100644
--- a/src/web/app/mobile/api/post.ts
+++ b/src/web/app/mobile/api/post.ts
@@ -1,26 +1,34 @@
 import PostForm from '../views/components/post-form.vue';
-import RepostForm from '../views/components/repost-form.vue';
+//import RepostForm from '../views/components/repost-form.vue';
+import getPostSummary from '../../../../common/get-post-summary';
 
-export default function(opts) {
+export default (os) => (opts) => {
 	const o = opts || {};
 
-	const app = document.getElementById('app');
-	app.style.display = 'none';
-
-	function recover() {
-		app.style.display = 'block';
-	}
-
 	if (o.repost) {
-		const vm = new RepostForm({
+		/*const vm = new RepostForm({
 			propsData: {
 				repost: o.repost
 			}
 		}).$mount();
 		vm.$once('cancel', recover);
 		vm.$once('post', recover);
-		document.body.appendChild(vm.$el);
+		document.body.appendChild(vm.$el);*/
+
+		const text = window.prompt(`「${getPostSummary(o.repost)}」をRepost`);
+		if (text == null) return;
+		os.api('posts/create', {
+			repost_id: o.repost.id,
+			text: text == '' ? undefined : text
+		});
 	} else {
+		const app = document.getElementById('app');
+		app.style.display = 'none';
+
+		function recover() {
+			app.style.display = 'block';
+		}
+
 		const vm = new PostForm({
 			propsData: {
 				reply: o.reply
@@ -30,4 +38,4 @@ export default function(opts) {
 		vm.$once('post', recover);
 		document.body.appendChild(vm.$el);
 	}
-}
+};
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index 1d25280d9..89a21631e 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -38,7 +38,7 @@ init((launch) => {
 		chooseDriveFile,
 		dialog,
 		input,
-		post,
+		post: post(os),
 		notify
 	}));
 

From 52c2d7c794c9580f7d2ba4cd5b8aee76e541f8f6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 02:37:04 +0900
Subject: [PATCH 253/286] wip

---
 src/web/app/mobile/views/components/index.ts  |  5 +++++
 .../app/mobile/views/components/post-form.vue | 22 +++++++++++--------
 .../{ui-header.vue => ui.header.vue}          |  7 +++---
 .../components/{ui-nav.vue => ui.nav.vue}     | 22 +++++++++----------
 src/web/app/mobile/views/components/ui.vue    | 16 ++++++++++----
 src/web/app/mobile/views/pages/drive.vue      |  3 ++-
 6 files changed, 47 insertions(+), 28 deletions(-)
 create mode 100644 src/web/app/mobile/views/components/index.ts
 rename src/web/app/mobile/views/components/{ui-header.vue => ui.header.vue} (97%)
 rename src/web/app/mobile/views/components/{ui-nav.vue => ui.nav.vue} (77%)

diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
new file mode 100644
index 000000000..f628dee88
--- /dev/null
+++ b/src/web/app/mobile/views/components/index.ts
@@ -0,0 +1,5 @@
+import Vue from 'vue';
+
+import ui from './ui.vue';
+
+Vue.component('mk-ui', ui);
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index 6c41a73b5..bba669229 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -3,27 +3,27 @@
 	<header>
 		<button class="cancel" @click="cancel">%fa:times%</button>
 		<div>
-			<span v-if="refs.text" class="text-count { over: refs.text.value.length > 1000 }">{ 1000 - refs.text.value.length }</span>
+			<span v-if="refs.text" class="text-count" :class="{ over: refs.text.value.length > 1000 }">{{ 1000 - refs.text.value.length }}</span>
 			<button class="submit" @click="post">%i18n:mobile.tags.mk-post-form.submit%</button>
 		</div>
 	</header>
 	<div class="form">
-		<mk-post-preview v-if="opts.reply" post={ opts.reply }/>
-		<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ opts.reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.post-placeholder%' }></textarea>
-		<div class="attaches" show={ files.length != 0 }>
+		<mk-post-preview v-if="reply" :post="reply"/>
+		<textarea v-model="text" :disabled="wait" :placeholder="reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.post-placeholder%'"></textarea>
+		<div class="attaches" v-show="files.length != 0">
 			<ul class="files" ref="attaches">
-				<li class="file" each={ files } data-id={ id }>
-					<div class="img" style="background-image: url({ url + '?thumbnail&size=128' })" @click="removeFile"></div>
+				<li class="file" v-for="file in files">
+					<div class="img" :style="`background-image: url(${file.url}?thumbnail&size=128)`" @click="removeFile(file)"></div>
 				</li>
 			</ul>
 		</div>
-		<mk-poll-editor v-if="poll" ref="poll" ondestroy={ onPollDestroyed }/>
+		<mk-poll-editor v-if="poll" ref="poll"/>
 		<mk-uploader @uploaded="attachMedia" @change="onChangeUploadings"/>
 		<button ref="upload" @click="selectFile">%fa:upload%</button>
 		<button ref="drive" @click="selectFileFromDrive">%fa:cloud%</button>
 		<button class="kao" @click="kao">%fa:R smile%</button>
 		<button class="poll" @click="addPoll">%fa:chart-pie%</button>
-		<input ref="file" type="file" accept="image/*" multiple="multiple" onchange={ changeFile }/>
+		<input ref="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
 	</div>
 </div>
 </template>
@@ -31,9 +31,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import Sortable from 'sortablejs';
-import getKao from '../../common/scripts/get-kao';
+import getKao from '../../../common/scripts/get-kao';
 
 export default Vue.extend({
+	props: ['reply'],
 	data() {
 		return {
 			posting: false,
@@ -77,6 +78,9 @@ export default Vue.extend({
 		cancel() {
 			this.$emit('cancel');
 			this.$destroy();
+		},
+		kao() {
+			this.text += getKao();
 		}
 	}
 });
diff --git a/src/web/app/mobile/views/components/ui-header.vue b/src/web/app/mobile/views/components/ui.header.vue
similarity index 97%
rename from src/web/app/mobile/views/components/ui-header.vue
rename to src/web/app/mobile/views/components/ui.header.vue
index 85fb45780..3479bd90b 100644
--- a/src/web/app/mobile/views/components/ui-header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -9,7 +9,9 @@
 			<h1>
 				<slot>Misskey</slot>
 			</h1>
-			<button v-if="func" @click="func" v-html="funcIcon"></button>
+			<button v-if="func" @click="func">
+				<slot name="funcIcon"></slot>
+			</button>
 		</div>
 	</div>
 </div>
@@ -19,11 +21,10 @@
 import Vue from 'vue';
 
 export default Vue.extend({
-	props: ['func', 'funcIcon'],
+	props: ['func'],
 	data() {
 		return {
 			func: null,
-			funcIcon: null,
 			hasUnreadNotifications: false,
 			hasUnreadMessagingMessages: false,
 			connection: null,
diff --git a/src/web/app/mobile/views/components/ui-nav.vue b/src/web/app/mobile/views/components/ui.nav.vue
similarity index 77%
rename from src/web/app/mobile/views/components/ui-nav.vue
rename to src/web/app/mobile/views/components/ui.nav.vue
index 1767e6224..020be1f3d 100644
--- a/src/web/app/mobile/views/components/ui-nav.vue
+++ b/src/web/app/mobile/views/components/ui.nav.vue
@@ -2,28 +2,28 @@
 <div class="mk-ui-nav" :style="{ display: isOpen ? 'block' : 'none' }">
 	<div class="backdrop" @click="parent.toggleDrawer"></div>
 	<div class="body">
-		<a class="me" v-if="os.isSignedIn" href={ '/' + I.username }>
-			<img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/>
-			<p class="name">{ I.name }</p>
-		</a>
+		<router-link class="me" v-if="os.isSignedIn" :to="`/${os.i.username}`">
+			<img class="avatar" :src="`${os.i.avatar_url}?thumbnail&size=128`" alt="avatar"/>
+			<p class="name">{{ os.i.name }}</p>
+		</router-link>
 		<div class="links">
 			<ul>
-				<li><a href="/">%fa:home%%i18n:mobile.tags.mk-ui-nav.home%%fa:angle-right%</a></li>
-				<li><a href="/i/notifications">%fa:R bell%%i18n:mobile.tags.mk-ui-nav.notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</a></li>
-				<li><a href="/i/messaging">%fa:R comments%%i18n:mobile.tags.mk-ui-nav.messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</a></li>
+				<li><router-link href="/">%fa:home%%i18n:mobile.tags.mk-ui-nav.home%%fa:angle-right%</router-link></li>
+				<li><router-link href="/i/notifications">%fa:R bell%%i18n:mobile.tags.mk-ui-nav.notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</router-link></li>
+				<li><router-link href="/i/messaging">%fa:R comments%%i18n:mobile.tags.mk-ui-nav.messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</router-link></li>
 			</ul>
 			<ul>
-				<li><a href={ _CH_URL_ } target="_blank">%fa:tv%%i18n:mobile.tags.mk-ui-nav.ch%%fa:angle-right%</a></li>
-				<li><a href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-ui-nav.drive%%fa:angle-right%</a></li>
+				<li><a :href="chUrl" target="_blank">%fa:tv%%i18n:mobile.tags.mk-ui-nav.ch%%fa:angle-right%</a></li>
+				<li><router-link href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-ui-nav.drive%%fa:angle-right%</router-link></li>
 			</ul>
 			<ul>
 				<li><a @click="search">%fa:search%%i18n:mobile.tags.mk-ui-nav.search%%fa:angle-right%</a></li>
 			</ul>
 			<ul>
-				<li><a href="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</a></li>
+				<li><router-link href="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</router-link></li>
 			</ul>
 		</div>
-		<a href={ aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
+		<a :href="aboutUrl"><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
 	</div>
 </div>
 </template>
diff --git a/src/web/app/mobile/views/components/ui.vue b/src/web/app/mobile/views/components/ui.vue
index a07c9ed5a..b936971ad 100644
--- a/src/web/app/mobile/views/components/ui.vue
+++ b/src/web/app/mobile/views/components/ui.vue
@@ -1,9 +1,10 @@
 <template>
 <div class="mk-ui">
-	<mk-ui-header :func="func" :func-icon="funcIcon">
+	<x-header :func="func">
+		<template slot="funcIcon"><slot name="funcIcon"></slot></template>
 		<slot name="header"></slot>
-	</mk-ui-header>
-	<mk-ui-nav :is-open="isDrawerOpening"/>
+	</x-header>
+	<x-nav :is-open="isDrawerOpening"/>
 	<div class="content">
 		<slot></slot>
 	</div>
@@ -13,8 +14,15 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import XHeader from './ui.header.vue';
+import XNav from './ui.nav.vue';
+
 export default Vue.extend({
-	props: ['title', 'func', 'funcIcon'],
+	components: {
+		XHeader,
+		XNav
+	},
+	props: ['title', 'func'],
 	data() {
 		return {
 			isDrawerOpening: false,
diff --git a/src/web/app/mobile/views/pages/drive.vue b/src/web/app/mobile/views/pages/drive.vue
index c4c22448c..1f442c224 100644
--- a/src/web/app/mobile/views/pages/drive.vue
+++ b/src/web/app/mobile/views/pages/drive.vue
@@ -1,10 +1,11 @@
 <template>
-<mk-ui :func="fn" func-icon="%fa:ellipsis-h%">
+<mk-ui :func="fn">
 	<span slot="header">
 		<template v-if="folder">%fa:R folder-open%{{ folder.name }}</template>
 		<template v-if="file"><mk-file-type-icon class="icon"/>{{ file.name }}</template>
 		<template v-else>%fa:cloud%%i18n:mobile.tags.mk-drive-page.drive%</template>
 	</span>
+	<template slot="funcIcon">%fa:ellipsis-h%</template>
 	<mk-drive
 		ref="browser"
 		:init-folder="folder"

From 937b6539e0323e45cb793cf59bd842673f73be8f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 02:41:29 +0900
Subject: [PATCH 254/286] wip

---
 src/web/app/desktop/views/pages/post.vue | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/web/app/desktop/views/pages/post.vue b/src/web/app/desktop/views/pages/post.vue
index 8b9f30f10..446fdbcbf 100644
--- a/src/web/app/desktop/views/pages/post.vue
+++ b/src/web/app/desktop/views/pages/post.vue
@@ -23,6 +23,8 @@ export default Vue.extend({
 	mounted() {
 		Progress.start();
 
+		// TODO: extract the fetch step for vue-router's caching
+
 		(this as any).api('posts/show', {
 			post_id: this.postId
 		}).then(post => {

From 5daae05bcf4d298e534d789502d254e7185ce652 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 03:11:24 +0900
Subject: [PATCH 255/286] wip

---
 src/web/app/desktop/views/pages/index.vue      |  2 +-
 src/web/app/mobile/script.ts                   |  2 +-
 src/web/app/mobile/views/components/ui.nav.vue |  2 +-
 src/web/app/mobile/views/components/ui.vue     |  1 +
 src/web/app/mobile/views/pages/home.vue        |  6 +++---
 src/web/app/mobile/views/pages/index.vue       | 16 ++++++++++++++++
 src/web/app/mobile/views/pages/user.vue        |  3 ++-
 src/web/app/mobile/views/pages/welcome.vue     |  5 +++++
 8 files changed, 30 insertions(+), 7 deletions(-)
 create mode 100644 src/web/app/mobile/views/pages/index.vue
 create mode 100644 src/web/app/mobile/views/pages/welcome.vue

diff --git a/src/web/app/desktop/views/pages/index.vue b/src/web/app/desktop/views/pages/index.vue
index 6b8739e30..0ea47d913 100644
--- a/src/web/app/desktop/views/pages/index.vue
+++ b/src/web/app/desktop/views/pages/index.vue
@@ -1,5 +1,5 @@
 <template>
-	<component :is="os.isSignedIn ? 'home' : 'welcome'"></component>
+<component :is="os.isSignedIn ? 'home' : 'welcome'"></component>
 </template>
 
 <script lang="ts">
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index 89a21631e..29ca21925 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -15,7 +15,7 @@ import post from './api/post';
 import notify from './api/notify';
 
 import MkIndex from './views/pages/index.vue';
-import MkUser from './views/pages/user/user.vue';
+import MkUser from './views/pages/user.vue';
 import MkSelectDrive from './views/pages/selectdrive.vue';
 import MkDrive from './views/pages/drive.vue';
 
diff --git a/src/web/app/mobile/views/components/ui.nav.vue b/src/web/app/mobile/views/components/ui.nav.vue
index 020be1f3d..3fccdda5e 100644
--- a/src/web/app/mobile/views/components/ui.nav.vue
+++ b/src/web/app/mobile/views/components/ui.nav.vue
@@ -78,7 +78,7 @@ export default Vue.extend({
 		search() {
 			const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%');
 			if (query == null || query == '') return;
-			this.page('/search?q=' + encodeURIComponent(query));
+			this.$router.push('/search?q=' + encodeURIComponent(query));
 		},
 		onReadAllNotifications() {
 			this.hasUnreadNotifications = false;
diff --git a/src/web/app/mobile/views/components/ui.vue b/src/web/app/mobile/views/components/ui.vue
index b936971ad..1e34c84e6 100644
--- a/src/web/app/mobile/views/components/ui.vue
+++ b/src/web/app/mobile/views/components/ui.vue
@@ -14,6 +14,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import MkNotify from './notify.vue';
 import XHeader from './ui.header.vue';
 import XNav from './ui.nav.vue';
 
diff --git a/src/web/app/mobile/views/pages/home.vue b/src/web/app/mobile/views/pages/home.vue
index 4313ab699..c81cbcadb 100644
--- a/src/web/app/mobile/views/pages/home.vue
+++ b/src/web/app/mobile/views/pages/home.vue
@@ -1,6 +1,7 @@
 <template>
-<mk-ui :func="fn" func-icon="%fa:pencil-alt%">
+<mk-ui :func="fn">
 	<span slot="header">%fa:home%%i18n:mobile.tags.mk-home.home%</span>
+	<template slot="funcIcon">%fa:pencil-alt%</template>
 	<mk-home @loaded="onHomeLoaded"/>
 </mk-ui>
 </template>
@@ -9,7 +10,6 @@
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 import getPostSummary from '../../../../../common/get-post-summary';
-import openPostForm from '../../scripts/open-post-form';
 
 export default Vue.extend({
 	data() {
@@ -38,7 +38,7 @@ export default Vue.extend({
 	},
 	methods: {
 		fn() {
-			openPostForm();
+			(this as any).apis.post();
 		},
 		onHomeLoaded() {
 			Progress.done();
diff --git a/src/web/app/mobile/views/pages/index.vue b/src/web/app/mobile/views/pages/index.vue
new file mode 100644
index 000000000..0ea47d913
--- /dev/null
+++ b/src/web/app/mobile/views/pages/index.vue
@@ -0,0 +1,16 @@
+<template>
+<component :is="os.isSignedIn ? 'home' : 'welcome'"></component>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Home from './home.vue';
+import Welcome from './welcome.vue';
+
+export default Vue.extend({
+	components: {
+		Home,
+		Welcome
+	}
+});
+</script>
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index 745de2c6e..2d1611726 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -1,6 +1,7 @@
 <template>
-<mk-ui :func="fn" func-icon="%fa:pencil-alt%">
+<mk-ui :func="fn">
 	<span slot="header" v-if="!fetching">%fa:user% {{user.name}}</span>
+	<template slot="funcIcon">%fa:pencil-alt%</template>
 	<div v-if="!fetching" :class="$style.user">
 		<header>
 			<div class="banner" :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=1024)` : ''"></div>
diff --git a/src/web/app/mobile/views/pages/welcome.vue b/src/web/app/mobile/views/pages/welcome.vue
new file mode 100644
index 000000000..959d8cfca
--- /dev/null
+++ b/src/web/app/mobile/views/pages/welcome.vue
@@ -0,0 +1,5 @@
+<template>
+<div>
+	<mk-signin/>
+</div>
+</template>

From 0df6f5a2535374ddaed3daad586872574bb4b283 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 05:05:19 +0900
Subject: [PATCH 256/286] wip

---
 src/web/app/app.vue                           |   2 +-
 src/web/app/common/mios.ts                    |   9 +-
 .../connect-failed.troubleshooter.vue         |   4 +-
 src/web/app/mobile/views/pages/welcome.vue    | 145 +++++++++++++++++-
 webpack/module/rules/base64.ts                |  18 ---
 webpack/webpack.config.ts                     |  13 +-
 6 files changed, 165 insertions(+), 26 deletions(-)
 delete mode 100644 webpack/module/rules/base64.ts

diff --git a/src/web/app/app.vue b/src/web/app/app.vue
index 497d47003..321e00393 100644
--- a/src/web/app/app.vue
+++ b/src/web/app/app.vue
@@ -1,3 +1,3 @@
 <template>
-	<router-view></router-view>
+	<router-view id="app"></router-view>
 </template>
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index a37c5d6f7..e3a66f5b1 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -1,3 +1,4 @@
+import Vue from 'vue';
 import { EventEmitter } from 'eventemitter3';
 import api from './scripts/api';
 import signout from './scripts/signout';
@@ -8,6 +9,8 @@ import ServerStreamManager from './scripts/streaming/server-stream-manager';
 import RequestsStreamManager from './scripts/streaming/requests-stream-manager';
 import MessagingIndexStreamManager from './scripts/streaming/messaging-index-stream-manager';
 
+import Err from '../common/views/components/connect-failed.vue';
+
 //#region environment variables
 declare const _VERSION_: string;
 declare const _LANG_: string;
@@ -214,8 +217,10 @@ export default class MiOS extends EventEmitter {
 			// When failure
 			.catch(() => {
 				// Render the error screen
-				//document.body.innerHTML = '<mk-error />';
-				//riot.mount('*');
+				document.body.innerHTML = '<div id="err"></div>';
+				new Vue({
+					render: createEl => createEl(Err)
+				}).$mount('#err');
 
 				Progress.done();
 			});
diff --git a/src/web/app/common/views/components/connect-failed.troubleshooter.vue b/src/web/app/common/views/components/connect-failed.troubleshooter.vue
index 49396d158..bede504b5 100644
--- a/src/web/app/common/views/components/connect-failed.troubleshooter.vue
+++ b/src/web/app/common/views/components/connect-failed.troubleshooter.vue
@@ -41,8 +41,8 @@ export default Vue.extend({
 		return {
 			network: navigator.onLine,
 			end: false,
-			internet: false,
-			server: false
+			internet: null,
+			server: null
 		};
 	},
 	mounted() {
diff --git a/src/web/app/mobile/views/pages/welcome.vue b/src/web/app/mobile/views/pages/welcome.vue
index 959d8cfca..84e5ae550 100644
--- a/src/web/app/mobile/views/pages/welcome.vue
+++ b/src/web/app/mobile/views/pages/welcome.vue
@@ -1,5 +1,146 @@
 <template>
-<div>
-	<mk-signin/>
+<div class="welcome">
+	<h1><b>Misskey</b>へようこそ</h1>
+	<p>Twitter風ミニブログSNS、Misskeyへようこそ。思ったことを投稿したり、タイムラインでみんなの投稿を読むこともできます。</p>
+	<div class="form">
+		<p>ログイン</p>
+		<div>
+			<form @submit.prevent="onSubmit">
+				<input v-model="username" type="text" pattern="^[a-zA-Z0-9-]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/>
+				<input v-model="password" type="password" placeholder="パスワード" required/>
+				<input v-if="user && user.two_factor_enabled" v-model="token" type="number" placeholder="トークン" required/>
+				<button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button>
+			</form>
+			<div>
+				<a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a>
+			</div>
+		</div>
+	</div>
+	<a href="/signup">アカウントを作成する</a>
 </div>
 </template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { apiUrl } from '../../../config';
+
+export default Vue.extend({
+	data() {
+		return {
+			signing: false,
+			user: null,
+			username: '',
+			password: '',
+			token: '',
+			apiUrl
+		};
+	},
+	mounted() {
+		document.documentElement.style.background = '#293946';
+	},
+	methods: {
+		onUsernameChange() {
+			(this as any).api('users/show', {
+				username: this.username
+			}).then(user => {
+				this.user = user;
+			});
+		},
+		onSubmit() {
+			this.signing = true;
+
+			(this as any).api('signin', {
+				username: this.username,
+				password: this.password,
+				token: this.user && this.user.two_factor_enabled ? this.token : undefined
+			}).then(() => {
+				location.reload();
+			}).catch(() => {
+				alert('something happened');
+				this.signing = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.welcome
+	padding 16px
+	margin 0 auto
+	max-width 500px
+
+	h1
+		margin 0
+		padding 8px
+		font-size 1.5em
+		font-weight normal
+		color #c3c6ca
+
+		& + p
+			margin 0 0 16px 0
+			padding 0 8px 0 8px
+			color #949fa9
+
+	.form
+		background #fff
+		border solid 1px rgba(0, 0, 0, 0.2)
+		border-radius 8px
+		overflow hidden
+
+		& + a
+			display block
+			margin-top 16px
+			text-align center
+
+		> p
+			margin 0
+			padding 12px 20px
+			color #555
+			background #f5f5f5
+			border-bottom solid 1px #ddd
+
+		> div
+
+			> form
+				padding 16px
+				border-bottom solid 1px #ddd
+
+				input
+					display block
+					padding 12px
+					margin 0 0 16px 0
+					width 100%
+					font-size 1em
+					color rgba(0, 0, 0, 0.7)
+					background #fff
+					outline none
+					border solid 1px #ddd
+					border-radius 4px
+
+				button
+					display block
+					width 100%
+					padding 10px
+					margin 0
+					color #333
+					font-size 1em
+					text-align center
+					text-decoration none
+					text-shadow 0 1px 0 rgba(255, 255, 255, 0.9)
+					background-image linear-gradient(#fafafa, #eaeaea)
+					border 1px solid #ddd
+					border-bottom-color #cecece
+					border-radius 4px
+
+					&:active
+						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)
+
+			> div
+				padding 16px
+				text-align center
+
+</style>
diff --git a/webpack/module/rules/base64.ts b/webpack/module/rules/base64.ts
deleted file mode 100644
index c2f6b9339..000000000
--- a/webpack/module/rules/base64.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Replace base64 symbols
- */
-
-import * as fs from 'fs';
-
-export default () => ({
-	enforce: 'pre',
-	test: /\.(vue|js)$/,
-	exclude: /node_modules/,
-	loader: 'string-replace-loader',
-	query: {
-		search: /%base64:(.+?)%/g,
-		replace: (_, key) => {
-			return fs.readFileSync(__dirname + '/../../../src/web/' + key, 'base64');
-		}
-	}
-});
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index bd8c6d120..76d298078 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -2,6 +2,7 @@
  * webpack configuration
  */
 
+import * as fs from 'fs';
 const minify = require('html-minifier').minify;
 import I18nReplacer from '../src/common/build/i18n';
 import { pattern as faPattern, replacement as faReplacement } from '../src/common/build/fa';
@@ -19,7 +20,11 @@ global['collapseSpacesReplacement'] = html => {
 		collapseWhitespace: true,
 		collapseInlineTagWhitespace: true,
 		keepClosingSlash: true
-	});
+	}).replace(/\t/g, '');
+};
+
+global['base64replacement'] = (_, key) => {
+	return fs.readFileSync(__dirname + '/../src/web/' + key, 'base64');
 };
 
 module.exports = Object.keys(langs).map(lang => {
@@ -59,6 +64,12 @@ module.exports = Object.keys(langs).map(lang => {
 						cssSourceMap: false,
 						preserveWhitespace: false
 					}
+				}, {
+					loader: 'replace',
+					query: {
+						search: /%base64:(.+?)%/g.toString(),
+						replace: 'base64replacement'
+					}
 				}, {
 					loader: 'webpack-replace-loader',
 					options: {

From 7f7fdbd678b020f147c18410f2d8b626ec02d27f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 05:16:38 +0900
Subject: [PATCH 257/286] wip

---
 .../views/components/connect-failed.vue       |  7 ++-
 src/web/app/mobile/script.ts                  |  2 +
 src/web/app/mobile/views/pages/signup.vue     | 57 +++++++++++++++++++
 3 files changed, 65 insertions(+), 1 deletion(-)
 create mode 100644 src/web/app/mobile/views/pages/signup.vue

diff --git a/src/web/app/common/views/components/connect-failed.vue b/src/web/app/common/views/components/connect-failed.vue
index 4761c6d6e..b48f7cecb 100644
--- a/src/web/app/common/views/components/connect-failed.vue
+++ b/src/web/app/common/views/components/connect-failed.vue
@@ -4,7 +4,7 @@
 	<h1>%i18n:common.tags.mk-error.title%</h1>
 	<p class="text">
 		{{ '%i18n:common.tags.mk-error.description%'.substr(0, '%i18n:common.tags.mk-error.description%'.indexOf('{')) }}
-		<a @click="location.reload()">{{ '%i18n:common.tags.mk-error.description%'.match(/\{(.+?)\}/)[1] }}</a>
+		<a @click="reload">{{ '%i18n:common.tags.mk-error.description%'.match(/\{(.+?)\}/)[1] }}</a>
 		{{ '%i18n:common.tags.mk-error.description%'.substr('%i18n:common.tags.mk-error.description%'.indexOf('}') + 1) }}
 	</p>
 	<button v-if="!troubleshooting" @click="troubleshooting = true">%i18n:common.tags.mk-error.troubleshoot%</button>
@@ -29,6 +29,11 @@ export default Vue.extend({
 	mounted() {
 		document.title = 'Oops!';
 		document.documentElement.style.background = '#f8f8f8';
+	},
+	methods: {
+		reload() {
+			location.reload();
+		}
 	}
 });
 </script>
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index 29ca21925..a2f118b8f 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -15,6 +15,7 @@ import post from './api/post';
 import notify from './api/notify';
 
 import MkIndex from './views/pages/index.vue';
+import MkSignup from './views/pages/signup.vue';
 import MkUser from './views/pages/user.vue';
 import MkSelectDrive from './views/pages/selectdrive.vue';
 import MkDrive from './views/pages/drive.vue';
@@ -45,6 +46,7 @@ init((launch) => {
 	// Routing
 	app.$router.addRoutes([
 		{ path: '/', name: 'index', component: MkIndex },
+		{ path: '/signup', name: 'signup', component: MkSignup },
 		{ path: '/i/drive', component: MkDrive },
 		{ path: '/i/drive/folder/:folder', component: MkDrive },
 		{ path: '/selectdrive', component: MkSelectDrive },
diff --git a/src/web/app/mobile/views/pages/signup.vue b/src/web/app/mobile/views/pages/signup.vue
new file mode 100644
index 000000000..9dc07a4b8
--- /dev/null
+++ b/src/web/app/mobile/views/pages/signup.vue
@@ -0,0 +1,57 @@
+<template>
+<div class="signup">
+	<h1>Misskeyをはじめる</h1>
+	<p>いつでも、どこからでもMisskeyを利用できます。もちろん、無料です。</p>
+	<div class="form">
+		<p>新規登録</p>
+		<div>
+			<mk-signup/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	mounted() {
+		document.documentElement.style.background = '#293946';
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.signup
+	padding 16px
+	margin 0 auto
+	max-width 500px
+
+	h1
+		margin 0
+		padding 8px
+		font-size 1.5em
+		font-weight normal
+		color #c3c6ca
+
+		& + p
+			margin 0 0 16px 0
+			padding 0 8px 0 8px
+			color #949fa9
+
+	.form
+		background #fff
+		border solid 1px rgba(0, 0, 0, 0.2)
+		border-radius 8px
+		overflow hidden
+
+		> p
+			margin 0
+			padding 12px 20px
+			color #555
+			background #f5f5f5
+			border-bottom solid 1px #ddd
+
+		> div
+			padding 16px
+
+</style>

From c30fff623d0d8f644184a6a4e040a08d098840ed Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 05:30:37 +0900
Subject: [PATCH 258/286] wip

---
 src/web/app/mobile/views/components/index.ts  |  6 ++
 .../{posts-post.vue => posts.post.vue}        | 57 ++++++++++---------
 src/web/app/mobile/views/components/posts.vue |  7 ++-
 .../app/mobile/views/components/ui.header.vue |  6 +-
 .../app/mobile/views/components/ui.nav.vue    |  4 +-
 5 files changed, 47 insertions(+), 33 deletions(-)
 rename src/web/app/mobile/views/components/{posts-post.vue => posts.post.vue} (78%)

diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index f628dee88..8462cdb3e 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -1,5 +1,11 @@
 import Vue from 'vue';
 
 import ui from './ui.vue';
+import home from './home.vue';
+import timeline from './timeline.vue';
+import posts from './posts.vue';
 
 Vue.component('mk-ui', ui);
+Vue.component('mk-home', home);
+Vue.component('mk-timeline', timeline);
+Vue.component('mk-posts', posts);
diff --git a/src/web/app/mobile/views/components/posts-post.vue b/src/web/app/mobile/views/components/posts.post.vue
similarity index 78%
rename from src/web/app/mobile/views/components/posts-post.vue
rename to src/web/app/mobile/views/components/posts.post.vue
index b252a6e97..225a530b5 100644
--- a/src/web/app/mobile/views/components/posts-post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -1,58 +1,62 @@
 <template>
-<div class="mk-posts-post" :class="{ repost: isRepost }">
+<div class="post" :class="{ repost: isRepost }">
 	<div class="reply-to" v-if="p.reply">
-		<mk-timeline-post-sub post={ p.reply }/>
+		<x-sub :post="p.reply"/>
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<a class="avatar-anchor" href={ '/' + post.user.username }>
-				<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-			</a>
-			%fa:retweet%{'%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{'))}<a class="name" href={ '/' + post.user.username }>{ post.user.name }</a>{'%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1)}
+			<router-link class="avatar-anchor" :to="`/${post.user.username}`">
+				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%
+			{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}
+			<router-link class="name" :to="`/${post.user.username}`">{{ post.user.name }}</router-link>
+			{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}
 		</p>
-		<mk-time time={ post.created_at }/>
+		<mk-time :time="post.created_at"/>
 	</div>
 	<article>
-		<a class="avatar-anchor" href={ '/' + p.user.username }>
-			<img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=96' } alt="avatar"/>
-		</a>
+		<router-link class="avatar-anchor" :to="`/${p.user.username}`">
+			<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=96`" alt="avatar"/>
+		</router-link>
 		<div class="main">
 			<header>
-				<a class="name" href={ '/' + p.user.username }>{ p.user.name }</a>
+				<router-link class="name" :to="`/${p.user.username}`">{{ p.user.name }}</router-link>
 				<span class="is-bot" v-if="p.user.is_bot">bot</span>
-				<span class="username">@{ p.user.username }</span>
-				<a class="created-at" href={ url }>
-					<mk-time time={ p.created_at }/>
-				</a>
+				<span class="username">@{{ p.user.username }}</span>
+				<router-link class="created-at" :to="url">
+					<mk-time :time="p.created_at"/>
+				</router-link>
 			</header>
 			<div class="body">
 				<div class="text" ref="text">
-					<p class="channel" v-if="p.channel != null"><a href={ _CH_URL_ + '/' + p.channel.id } target="_blank">{ p.channel.title }</a>:</p>
+					<p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p>
 					<a class="reply" v-if="p.reply">
 						%fa:reply%
 					</a>
-					<p class="dummy"></p>
+					<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i"/>
+					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 					<a class="quote" v-if="p.repost != null">RP:</a>
 				</div>
 				<div class="media" v-if="p.media">
-					<mk-images images={ p.media }/>
+					<mk-images :images="p.media"/>
 				</div>
-				<mk-poll v-if="p.poll" post={ p } ref="pollViewer"/>
-				<span class="app" v-if="p.app">via <b>{ p.app.name }</b></span>
+				<mk-poll v-if="p.poll" :post="p" ref="pollViewer"/>
+				<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
 				<div class="repost" v-if="p.repost">%fa:quote-right -flip-h%
-					<mk-post-preview class="repost" post={ p.repost }/>
+					<mk-post-preview class="repost" :post="p.repost"/>
 				</div>
 			</div>
 			<footer>
-				<mk-reactions-viewer post={ p } ref="reactionsViewer"/>
+				<mk-reactions-viewer :post="p" ref="reactionsViewer"/>
 				<button @click="reply">
-					%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
+					%fa:reply%<p class="count" v-if="p.replies_count > 0">{{ p.replies_count }}</p>
 				</button>
 				<button @click="repost" title="Repost">
-					%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
+					%fa:retweet%<p class="count" v-if="p.repost_count > 0">{{ p.repost_count }}</p>
 				</button>
 				<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton">
-					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
+					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
 				</button>
 				<button class="menu" @click="menu" ref="menuButton">
 					%fa:ellipsis-h%
@@ -65,7 +69,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import openPostForm from '../scripts/open-post-form';
 
 export default Vue.extend({
 	props: ['post'],
@@ -154,7 +157,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-posts-post
+.post
 	font-size 12px
 	border-bottom solid 1px #eaeaea
 
diff --git a/src/web/app/mobile/views/components/posts.vue b/src/web/app/mobile/views/components/posts.vue
index e3abd9ca6..01897eafd 100644
--- a/src/web/app/mobile/views/components/posts.vue
+++ b/src/web/app/mobile/views/components/posts.vue
@@ -2,7 +2,7 @@
 <div class="mk-posts">
 	<slot name="head"></slot>
 	<template v-for="(post, i) in _posts">
-		<mk-posts-post :post="post" :key="post.id"/>
+		<x-post :post="post" :key="post.id"/>
 		<p class="date" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date">
 			<span>%fa:angle-up%{{ post._datetext }}</span>
 			<span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span>
@@ -14,7 +14,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import XPost from './posts.post.vue';
+
 export default Vue.extend({
+	components: {
+		XPost
+	},
 	props: {
 		posts: {
 			type: Array,
diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/web/app/mobile/views/components/ui.header.vue
index 3479bd90b..b9b7b4771 100644
--- a/src/web/app/mobile/views/components/ui.header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -1,10 +1,10 @@
 <template>
-<div class="mk-ui-header">
+<div class="header">
 	<mk-special-message/>
 	<div class="main">
 		<div class="backdrop"></div>
 		<div class="content">
-			<button class="nav" @click="parent.toggleDrawer">%fa:bars%</button>
+			<button class="nav" @click="$parent.isDrawerOpening = true">%fa:bars%</button>
 			<template v-if="hasUnreadNotifications || hasUnreadMessagingMessages">%fa:circle%</template>
 			<h1>
 				<slot>Misskey</slot>
@@ -83,7 +83,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-ui-header
+.header
 	$height = 48px
 
 	position fixed
diff --git a/src/web/app/mobile/views/components/ui.nav.vue b/src/web/app/mobile/views/components/ui.nav.vue
index 3fccdda5e..3796b2765 100644
--- a/src/web/app/mobile/views/components/ui.nav.vue
+++ b/src/web/app/mobile/views/components/ui.nav.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-ui-nav" :style="{ display: isOpen ? 'block' : 'none' }">
+<div class="nav" :style="{ display: isOpen ? 'block' : 'none' }">
 	<div class="backdrop" @click="parent.toggleDrawer"></div>
 	<div class="body">
 		<router-link class="me" v-if="os.isSignedIn" :to="`/${os.i.username}`">
@@ -97,7 +97,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-ui-nav
+.nav
 	.backdrop
 		position fixed
 		top 0

From 0b1bcf614ba2467fdc638354418f1077a20919ad Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 05:57:24 +0900
Subject: [PATCH 259/286] wip

---
 .../common/views/components/poll-editor.vue   | 12 ++++-
 .../desktop/views/components/images-image.vue | 35 +++++++--------
 .../desktop/views/components/post-form.vue    | 44 +++++++++++--------
 3 files changed, 50 insertions(+), 41 deletions(-)

diff --git a/src/web/app/common/views/components/poll-editor.vue b/src/web/app/common/views/components/poll-editor.vue
index 7428d8054..065e91966 100644
--- a/src/web/app/common/views/components/poll-editor.vue
+++ b/src/web/app/common/views/components/poll-editor.vue
@@ -4,7 +4,7 @@
 		%fa:exclamation-triangle%%i18n:common.tags.mk-poll-editor.no-only-one-choice%
 	</p>
 	<ul ref="choices">
-		<li v-for="(choice, i) in choices" :key="choice">
+		<li v-for="(choice, i) in choices">
 			<input :value="choice" @input="onInput(i, $event)" :placeholder="'%i18n:common.tags.mk-poll-editor.choice-n%'.replace('{}', i + 1)">
 			<button @click="remove(i)" title="%i18n:common.tags.mk-poll-editor.remove%">
 				%fa:times%
@@ -26,6 +26,11 @@ export default Vue.extend({
 			choices: ['', '']
 		};
 	},
+	watch: {
+		choices() {
+			this.$emit('updated');
+		}
+	},
 	methods: {
 		onInput(i, e) {
 			Vue.set(this.choices, i, e.target.value);
@@ -33,7 +38,9 @@ export default Vue.extend({
 
 		add() {
 			this.choices.push('');
-			(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
+			this.$nextTick(() => {
+				(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
+			});
 		},
 
 		remove(i) {
@@ -53,6 +60,7 @@ export default Vue.extend({
 		set(data) {
 			if (data.choices.length == 0) return;
 			this.choices = data.choices;
+			if (data.choices.length == 1) this.choices = this.choices.concat('');
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/images-image.vue b/src/web/app/desktop/views/components/images-image.vue
index b29428ac3..cb6c529f7 100644
--- a/src/web/app/desktop/views/components/images-image.vue
+++ b/src/web/app/desktop/views/components/images-image.vue
@@ -1,14 +1,12 @@
 <template>
-<div>
-	<a class="mk-images-image"
-		:href="image.url"
-		@mousemove="onMousemove"
-		@mouseleave="onMouseleave"
-		@click.prevent="onClick"
-		:style="style"
-		:title="image.name"
-	></a>
-</div>
+<a class="mk-images-image"
+	:href="image.url"
+	@mousemove="onMousemove"
+	@mouseleave="onMouseleave"
+	@click.prevent="onClick"
+	:style="style"
+	:title="image.name"
+></a>
 </template>
 
 <script lang="ts">
@@ -53,18 +51,15 @@ export default Vue.extend({
 
 <style lang="stylus" scoped>
 .mk-images-image
+	display block
+	cursor zoom-in
 	overflow hidden
+	width 100%
+	height 100%
+	background-position center
 	border-radius 4px
 
-	> a
-		display block
-		cursor zoom-in
-		overflow hidden
-		width 100%
-		height 100%
-		background-position center
-
-		&:not(:hover)
-			background-size cover
+	&:not(:hover)
+		background-size cover
 
 </style>
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index c362d500e..23006d338 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -11,15 +11,15 @@
 			@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
 		></textarea>
 		<div class="medias" :class="{ with: poll }" v-show="files.length != 0">
-			<ul ref="media">
-				<li v-for="file in files" :key="file.id">
+			<x-draggable :list="files" :options="{ animation: 150 }">
+				<div v-for="file in files" :key="file.id">
 					<div class="img" :style="{ backgroundImage: `url(${file.url}?thumbnail&size=64)` }" :title="file.name"></div>
 					<img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/>
-				</li>
-			</ul>
+				</div>
+			</x-draggable>
 			<p class="remain">{{ 4 - files.length }}/4</p>
 		</div>
-		<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false"/>
+		<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="saveDraft()"/>
 	</div>
 	<mk-uploader @uploaded="attachMedia" @change="onChangeUploadings"/>
 	<button class="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" @click="chooseFile">%fa:upload%</button>
@@ -37,11 +37,14 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import * as Sortable from 'sortablejs';
+import * as XDraggable from 'vuedraggable';
 import Autocomplete from '../../scripts/autocomplete';
 import getKao from '../../../common/scripts/get-kao';
 
 export default Vue.extend({
+	components: {
+		XDraggable
+	},
 	props: ['reply', 'repost'],
 	data() {
 		return {
@@ -80,6 +83,17 @@ export default Vue.extend({
 			return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.repost);
 		}
 	},
+	watch: {
+		text() {
+			this.saveDraft();
+		},
+		poll() {
+			this.saveDraft();
+		},
+		files() {
+			this.saveDraft();
+		}
+	},
 	mounted() {
 		this.$nextTick(() => {
 			this.autocomplete = new Autocomplete(this.$refs.text);
@@ -92,14 +106,12 @@ export default Vue.extend({
 				this.files = draft.data.files;
 				if (draft.data.poll) {
 					this.poll = true;
-					(this.$refs.poll as any).set(draft.data.poll);
+					this.$nextTick(() => {
+						(this.$refs.poll as any).set(draft.data.poll);
+					});
 				}
 				this.$emit('change-attached-media', this.files);
 			}
-
-			new Sortable(this.$refs.media, {
-				animation: 150
-			});
 		});
 	},
 	beforeDestroy() {
@@ -322,22 +334,16 @@ export default Vue.extend({
 				padding 0
 				color rgba($theme-color, 0.4)
 
-			> ul
-				display block
-				margin 0
+			> div
 				padding 4px
-				list-style none
 
 				&:after
 					content ""
 					display block
 					clear both
 
-				> li
-					display block
+				> div
 					float left
-					margin 0
-					padding 0
 					border solid 4px transparent
 					cursor move
 

From 8056497809754197af2d67b0b2c65f1a57122d06 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 06:13:38 +0900
Subject: [PATCH 260/286] wip

---
 .../desktop/views/components/post-form.vue    |  5 +-
 .../directives}/autocomplete.ts               | 46 +++++++++++--------
 src/web/app/desktop/views/directives/index.ts |  2 +
 3 files changed, 31 insertions(+), 22 deletions(-)
 rename src/web/app/desktop/{scripts => views/directives}/autocomplete.ts (78%)

diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 23006d338..f63584806 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -9,6 +9,7 @@
 		<textarea :class="{ with: (files.length != 0 || poll) }"
 			ref="text" v-model="text" :disabled="posting"
 			@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
+			v-autocomplete
 		></textarea>
 		<div class="medias" :class="{ with: poll }" v-show="files.length != 0">
 			<x-draggable :list="files" :options="{ animation: 150 }">
@@ -38,7 +39,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as XDraggable from 'vuedraggable';
-import Autocomplete from '../../scripts/autocomplete';
 import getKao from '../../../common/scripts/get-kao';
 
 export default Vue.extend({
@@ -96,9 +96,6 @@ export default Vue.extend({
 	},
 	mounted() {
 		this.$nextTick(() => {
-			this.autocomplete = new Autocomplete(this.$refs.text);
-			this.autocomplete.attach();
-
 			// 書きかけの投稿を復元
 			const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
 			if (draft) {
diff --git a/src/web/app/desktop/scripts/autocomplete.ts b/src/web/app/desktop/views/directives/autocomplete.ts
similarity index 78%
rename from src/web/app/desktop/scripts/autocomplete.ts
rename to src/web/app/desktop/views/directives/autocomplete.ts
index 8f075efdd..35a3b2e9c 100644
--- a/src/web/app/desktop/scripts/autocomplete.ts
+++ b/src/web/app/desktop/views/directives/autocomplete.ts
@@ -1,5 +1,18 @@
-import getCaretCoordinates from 'textarea-caret';
-import * as riot from 'riot';
+import * as getCaretCoordinates from 'textarea-caret';
+import MkAutocomplete from '../components/autocomplete.vue';
+
+export default {
+	bind(el, binding, vn) {
+		const self = el._userPreviewDirective_ = {} as any;
+		self.x = new Autocomplete(el);
+		self.x.attach();
+	},
+
+	unbind(el, binding, vn) {
+		const self = el._userPreviewDirective_;
+		self.x.close();
+	}
+};
 
 /**
  * オートコンプリートを管理するクラス。
@@ -65,7 +78,15 @@ class Autocomplete {
 		this.close();
 
 		// サジェスト要素作成
-		const tag = document.createElement('mk-autocomplete-suggestion');
+		this.suggestion = new MkAutocomplete({
+			propsData: {
+				textarea: this.textarea,
+				complete: this.complete,
+				close: this.close,
+				type: type,
+				q: q
+			}
+		}).$mount();
 
 		// ~ サジェストを表示すべき位置を計算 ~
 
@@ -76,20 +97,11 @@ class Autocomplete {
 		const x = rect.left + window.pageXOffset + caretPosition.left;
 		const y = rect.top + window.pageYOffset + caretPosition.top;
 
-		tag.style.left = x + 'px';
-		tag.style.top = y + 'px';
+		this.suggestion.$el.style.left = x + 'px';
+		this.suggestion.$el.style.top = y + 'px';
 
 		// 要素追加
-		const el = document.body.appendChild(tag);
-
-		// マウント
-		this.suggestion = (riot as any).mount(el, {
-			textarea: this.textarea,
-			complete: this.complete,
-			close: this.close,
-			type: type,
-			q: q
-		})[0];
+		document.body.appendChild(this.suggestion.$el);
 	}
 
 	/**
@@ -98,7 +110,7 @@ class Autocomplete {
 	private close() {
 		if (this.suggestion == null) return;
 
-		this.suggestion.unmount();
+		this.suggestion.$destroy();
 		this.suggestion = null;
 
 		this.textarea.focus();
@@ -128,5 +140,3 @@ class Autocomplete {
 		this.textarea.setSelectionRange(pos, pos);
 	}
 }
-
-export default Autocomplete;
diff --git a/src/web/app/desktop/views/directives/index.ts b/src/web/app/desktop/views/directives/index.ts
index 324e07596..3d0c73b6b 100644
--- a/src/web/app/desktop/views/directives/index.ts
+++ b/src/web/app/desktop/views/directives/index.ts
@@ -1,6 +1,8 @@
 import Vue from 'vue';
 
 import userPreview from './user-preview';
+import autocomplete from './autocomplete';
 
 Vue.directive('userPreview', userPreview);
 Vue.directive('user-preview', userPreview);
+Vue.directive('autocomplete', autocomplete);

From f5637205cbaabaec7a9d40acca0d2a298dfac71d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 06:17:02 +0900
Subject: [PATCH 261/286] wip

---
 .../app/mobile/views/components/images-image.vue | 16 +++++-----------
 src/web/app/mobile/views/components/index.ts     |  2 ++
 2 files changed, 7 insertions(+), 11 deletions(-)

diff --git a/src/web/app/mobile/views/components/images-image.vue b/src/web/app/mobile/views/components/images-image.vue
index e89923492..6bc1dc0ae 100644
--- a/src/web/app/mobile/views/components/images-image.vue
+++ b/src/web/app/mobile/views/components/images-image.vue
@@ -1,7 +1,5 @@
 <template>
-<div>
-	<a class="mk-images-image" :href="image.url" target="_blank" :style="style" :title="image.name"></a>
-</div>
+<a class="mk-images-image" :href="image.url" target="_blank" :style="style" :title="image.name"></a>
 </template>
 
 <script lang="ts">
@@ -24,14 +22,10 @@ export default Vue.extend({
 .mk-images-image
 	display block
 	overflow hidden
+	width 100%
+	height 100%
+	background-position center
+	background-size cover
 	border-radius 4px
 
-	> a
-		display block
-		overflow hidden
-		width 100%
-		height 100%
-		background-position center
-		background-size cover
-
 </style>
diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index 8462cdb3e..c90275d68 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -4,8 +4,10 @@ import ui from './ui.vue';
 import home from './home.vue';
 import timeline from './timeline.vue';
 import posts from './posts.vue';
+import imagesImage from './images-image.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-home', home);
 Vue.component('mk-timeline', timeline);
 Vue.component('mk-posts', posts);
+Vue.component('mk-images-image', imagesImage);

From 7a97924d013ad0fbea6c4b20a478651320c836a0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 07:06:47 +0900
Subject: [PATCH 262/286] wip

---
 src/web/app/common/views/components/index.ts  |   2 +
 .../views/components/messaging-room.vue       |   4 +-
 .../app/common/views/components/post-menu.vue |  59 ++++-----
 .../desktop/views/components/posts.post.vue   |   5 +-
 .../views/components/drive-file-chooser.vue   |   4 +-
 .../mobile/views/components/drive.folder.vue  |   4 +-
 src/web/app/mobile/views/components/drive.vue |  25 ++--
 src/web/app/mobile/views/components/index.ts  |   2 +
 .../app/mobile/views/components/post-form.vue |  73 +++++++----
 .../views/components/posts-post-sub.vue       | 117 ------------------
 .../views/components/posts.post.sub.vue       | 108 ++++++++++++++++
 .../mobile/views/components/posts.post.vue    |  34 +++++
 .../app/mobile/views/components/timeline.vue  |   2 +-
 .../app/mobile/views/components/ui.header.vue |   1 -
 .../app/mobile/views/components/ui.nav.vue    |  20 +--
 15 files changed, 265 insertions(+), 195 deletions(-)
 delete mode 100644 src/web/app/mobile/views/components/posts-post-sub.vue
 create mode 100644 src/web/app/mobile/views/components/posts.post.sub.vue

diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index bde313910..d3f6a425f 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -18,6 +18,7 @@ import messaging from './messaging.vue';
 import messagingRoom from './messaging-room.vue';
 import urlPreview from './url-preview.vue';
 import twitterSetting from './twitter-setting.vue';
+import fileTypeIcon from './file-type-icon.vue';
 
 Vue.component('mk-signin', signin);
 Vue.component('mk-signup', signup);
@@ -37,3 +38,4 @@ Vue.component('mk-messaging', messaging);
 Vue.component('mk-messaging-room', messagingRoom);
 Vue.component('mk-url-preview', urlPreview);
 Vue.component('mk-twitter-setting', twitterSetting);
+Vue.component('mk-file-type-icon', fileTypeIcon);
diff --git a/src/web/app/common/views/components/messaging-room.vue b/src/web/app/common/views/components/messaging-room.vue
index 5022655a2..cfb1e23ac 100644
--- a/src/web/app/common/views/components/messaging-room.vue
+++ b/src/web/app/common/views/components/messaging-room.vue
@@ -15,7 +15,7 @@
 		</template>
 	</div>
 	<footer>
-		<div ref="notifications"></div>
+		<div ref="notifications" class="notifications"></div>
 		<div class="grippie" title="%i18n:common.tags.mk-messaging-room.resize-form%"></div>
 		<x-form :user="user"/>
 	</footer>
@@ -278,7 +278,7 @@ export default Vue.extend({
 		background rgba(255, 255, 255, 0.95)
 		background-clip content-box
 
-		> [ref='notifications']
+		> .notifications
 			position absolute
 			top -48px
 			width 100%
diff --git a/src/web/app/common/views/components/post-menu.vue b/src/web/app/common/views/components/post-menu.vue
index e14d67fc8..a53680e55 100644
--- a/src/web/app/common/views/components/post-menu.vue
+++ b/src/web/app/common/views/components/post-menu.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="mk-post-menu">
 	<div class="backdrop" ref="backdrop" @click="close"></div>
-	<div class="popover { compact: opts.compact }" ref="popover">
-		<button v-if="post.user_id === I.id" @click="pin">%i18n:common.tags.mk-post-menu.pin%</button>
+	<div class="popover" :class="{ compact }" ref="popover">
+		<button v-if="post.user_id == os.i.id" @click="pin">%i18n:common.tags.mk-post-menu.pin%</button>
 	</div>
 </div>
 </template>
@@ -14,36 +14,38 @@ import * as anime from 'animejs';
 export default Vue.extend({
 	props: ['post', 'source', 'compact'],
 	mounted() {
-		const popover = this.$refs.popover as any;
+		this.$nextTick(() => {
+			const popover = this.$refs.popover as any;
 
-		const rect = this.source.getBoundingClientRect();
-		const width = popover.offsetWidth;
-		const height = popover.offsetHeight;
+			const rect = this.source.getBoundingClientRect();
+			const width = popover.offsetWidth;
+			const height = popover.offsetHeight;
 
-		if (this.compact) {
-			const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-			const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
-			popover.style.left = (x - (width / 2)) + 'px';
-			popover.style.top = (y - (height / 2)) + 'px';
-		} else {
-			const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-			const y = rect.top + window.pageYOffset + this.source.offsetHeight;
-			popover.style.left = (x - (width / 2)) + 'px';
-			popover.style.top = y + 'px';
-		}
+			if (this.compact) {
+				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+				const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
+				popover.style.left = (x - (width / 2)) + 'px';
+				popover.style.top = (y - (height / 2)) + 'px';
+			} else {
+				const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+				const y = rect.top + window.pageYOffset + this.source.offsetHeight;
+				popover.style.left = (x - (width / 2)) + 'px';
+				popover.style.top = y + 'px';
+			}
 
-		anime({
-			targets: this.$refs.backdrop,
-			opacity: 1,
-			duration: 100,
-			easing: 'linear'
-		});
+			anime({
+				targets: this.$refs.backdrop,
+				opacity: 1,
+				duration: 100,
+				easing: 'linear'
+			});
 
-		anime({
-			targets: this.$refs.popover,
-			opacity: 1,
-			scale: [0.5, 1],
-			duration: 500
+			anime({
+				targets: this.$refs.popover,
+				opacity: 1,
+				scale: [0.5, 1],
+				duration: 500
+			});
 		});
 	},
 	methods: {
@@ -134,5 +136,6 @@ $border-color = rgba(27, 31, 35, 0.15)
 
 		> button
 			display block
+			padding 16px
 
 </style>
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 92218ead3..c757cbc7f 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -8,7 +8,10 @@
 			<router-link class="avatar-anchor" :to="`/${post.user.username}`" v-user-preview="post.user_id">
 				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
-			%fa:retweet%{{'%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{'))}}<a class="name" :href="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</a>{{'%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1)}}
+			%fa:retweet%
+			{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}
+			<a class="name" :href="`/${post.user.username}`" v-user-preview="post.user_id">{{ post.user.name }}</a>
+			{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}
 		</p>
 		<mk-time :time="post.created_at"/>
 	</div>
diff --git a/src/web/app/mobile/views/components/drive-file-chooser.vue b/src/web/app/mobile/views/components/drive-file-chooser.vue
index 6f1d25f63..6806af0f1 100644
--- a/src/web/app/mobile/views/components/drive-file-chooser.vue
+++ b/src/web/app/mobile/views/components/drive-file-chooser.vue
@@ -4,10 +4,10 @@
 		<header>
 			<h1>%i18n:mobile.tags.mk-drive-selector.select-file%<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1>
 			<button class="close" @click="cancel">%fa:times%</button>
-			<button v-if="opts.multiple" class="ok" @click="ok">%fa:check%</button>
+			<button v-if="multiple" class="ok" @click="ok">%fa:check%</button>
 		</header>
 		<mk-drive ref="browser"
-			select-file
+			:select-file="true"
 			:multiple="multiple"
 			@change-selection="onChangeSelection"
 			@selected="onSelected"
diff --git a/src/web/app/mobile/views/components/drive.folder.vue b/src/web/app/mobile/views/components/drive.folder.vue
index b776af7aa..22ff38fec 100644
--- a/src/web/app/mobile/views/components/drive.folder.vue
+++ b/src/web/app/mobile/views/components/drive.folder.vue
@@ -1,5 +1,5 @@
 <template>
-<a class="folder" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`">
+<a class="root folder" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`">
 	<div class="container">
 		<p class="name">%fa:folder%{{ folder.name }}</p>%fa:angle-right%
 	</div>
@@ -24,7 +24,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.folder
+.root.folder
 	display block
 	color #777
 	text-decoration none !important
diff --git a/src/web/app/mobile/views/components/drive.vue b/src/web/app/mobile/views/components/drive.vue
index f334f2241..35d91d183 100644
--- a/src/web/app/mobile/views/components/drive.vue
+++ b/src/web/app/mobile/views/components/drive.vue
@@ -26,11 +26,11 @@
 			</p>
 		</div>
 		<div class="folders" v-if="folders.length > 0">
-			<mk-drive-folder v-for="folder in folders" :key="folder.id" :folder="folder"/>
+			<x-folder v-for="folder in folders" :key="folder.id" :folder="folder"/>
 			<p v-if="moreFolders">%i18n:mobile.tags.mk-drive.load-more%</p>
 		</div>
 		<div class="files" v-if="files.length > 0">
-			<mk-drive-file v-for="file in files" :key="file.id" :file="file"/>
+			<x-file v-for="file in files" :key="file.id" :file="file"/>
 			<button class="more" v-if="moreFiles" @click="fetchMoreFiles">
 				{{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-drive.load-more%' }}
 			</button>
@@ -46,15 +46,23 @@
 			<div class="dot2"></div>
 		</div>
 	</div>
-	<input ref="file" type="file" multiple="multiple" @change="onChangeLocalFile"/>
-	<mk-drive-file-detail v-if="file != null" :file="file"/>
+	<input ref="file" class="file" type="file" multiple="multiple" @change="onChangeLocalFile"/>
+	<x-file-detail v-if="file != null" :file="file"/>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import XFolder from './drive.folder.vue';
+import XFile from './drive.file.vue';
+import XFileDetail from './drive.file-detail.vue';
 
 export default Vue.extend({
+	components: {
+		XFolder,
+		XFile,
+		XFileDetail
+	},
 	props: ['initFolder', 'initFile', 'selectFile', 'multiple', 'isNaked', 'top'],
 	data() {
 		return {
@@ -423,8 +431,7 @@ export default Vue.extend({
 				alert('現在いる場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。');
 				return;
 			}
-			const dialog = riot.mount(document.body.appendChild(document.createElement('mk-drive-folder-selector')))[0];
-			dialog.one('selected', folder => {
+			(this as any).apis.chooseDriveFolder().then(folder => {
 				(this as any).api('drive/folders/update', {
 					parent_id: folder ? folder.id : null,
 					folder_id: this.folder.id
@@ -510,11 +517,11 @@ export default Vue.extend({
 				color #777
 
 		> .folders
-			> .mk-drive-folder
+			> .folder
 				border-bottom solid 1px #eee
 
 		> .files
-			> .mk-drive-file
+			> .file
 				border-bottom solid 1px #eee
 
 			> .more
@@ -568,7 +575,7 @@ export default Vue.extend({
 			}
 		}
 
-	> [ref='file']
+	> .file
 		display none
 
 </style>
diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index c90275d68..715e291a7 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -5,9 +5,11 @@ import home from './home.vue';
 import timeline from './timeline.vue';
 import posts from './posts.vue';
 import imagesImage from './images-image.vue';
+import drive from './drive.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-home', home);
 Vue.component('mk-timeline', timeline);
 Vue.component('mk-posts', posts);
 Vue.component('mk-images-image', imagesImage);
+Vue.component('mk-drive', drive);
diff --git a/src/web/app/mobile/views/components/post-form.vue b/src/web/app/mobile/views/components/post-form.vue
index bba669229..3e8206c92 100644
--- a/src/web/app/mobile/views/components/post-form.vue
+++ b/src/web/app/mobile/views/components/post-form.vue
@@ -3,37 +3,40 @@
 	<header>
 		<button class="cancel" @click="cancel">%fa:times%</button>
 		<div>
-			<span v-if="refs.text" class="text-count" :class="{ over: refs.text.value.length > 1000 }">{{ 1000 - refs.text.value.length }}</span>
-			<button class="submit" @click="post">%i18n:mobile.tags.mk-post-form.submit%</button>
+			<span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span>
+			<button class="submit" :disabled="posting" @click="post">%i18n:mobile.tags.mk-post-form.submit%</button>
 		</div>
 	</header>
 	<div class="form">
 		<mk-post-preview v-if="reply" :post="reply"/>
-		<textarea v-model="text" :disabled="wait" :placeholder="reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.post-placeholder%'"></textarea>
+		<textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.post-placeholder%'"></textarea>
 		<div class="attaches" v-show="files.length != 0">
-			<ul class="files" ref="attaches">
-				<li class="file" v-for="file in files">
-					<div class="img" :style="`background-image: url(${file.url}?thumbnail&size=128)`" @click="removeFile(file)"></div>
-				</li>
-			</ul>
+			<x-draggable class="files" :list="files" :options="{ animation: 150 }">
+				<div class="file" v-for="file in files" :key="file.id">
+					<div class="img" :style="`background-image: url(${file.url}?thumbnail&size=128)`" @click="detachMedia(file)"></div>
+				</div>
+			</x-draggable>
 		</div>
 		<mk-poll-editor v-if="poll" ref="poll"/>
-		<mk-uploader @uploaded="attachMedia" @change="onChangeUploadings"/>
-		<button ref="upload" @click="selectFile">%fa:upload%</button>
-		<button ref="drive" @click="selectFileFromDrive">%fa:cloud%</button>
+		<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
+		<button class="upload" @click="chooseFile">%fa:upload%</button>
+		<button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button>
 		<button class="kao" @click="kao">%fa:R smile%</button>
-		<button class="poll" @click="addPoll">%fa:chart-pie%</button>
-		<input ref="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
+		<button class="poll" @click="poll = true">%fa:chart-pie%</button>
+		<input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
 	</div>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import Sortable from 'sortablejs';
+import * as XDraggable from 'vuedraggable';
 import getKao from '../../../common/scripts/get-kao';
 
 export default Vue.extend({
+	components: {
+		XDraggable
+	},
 	props: ['reply'],
 	data() {
 		return {
@@ -45,19 +48,27 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		(this.$refs.text as any).focus();
-
-		new Sortable(this.$refs.attaches, {
-			animation: 150
+		this.$nextTick(() => {
+			(this.$refs.text as any).focus();
 		});
 	},
 	methods: {
+		chooseFile() {
+			(this.$refs.file as any).click();
+		},
+		chooseFileFromDrive() {
+			(this as any).apis.chooseDriveFile({
+				multiple: true
+			}).then(files => {
+				files.forEach(this.attachMedia);
+			});
+		},
 		attachMedia(driveFile) {
 			this.files.push(driveFile);
 			this.$emit('change-attached-media', this.files);
 		},
-		detachMedia(id) {
-			this.files = this.files.filter(x => x.id != id);
+		detachMedia(file) {
+			this.files = this.files.filter(x => x.id != file.id);
 			this.$emit('change-attached-media', this.files);
 		},
 		onChangeFile() {
@@ -75,6 +86,20 @@ export default Vue.extend({
 			this.poll = false;
 			this.$emit('change-attached-media');
 		},
+		post() {
+			this.posting = true;
+			(this as any).api('posts/create', {
+				text: this.text == '' ? undefined : this.text,
+				media_ids: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
+				reply_id: this.reply ? this.reply.id : undefined,
+				poll: this.poll ? (this.$refs.poll as any).get() : undefined
+			}).then(data => {
+				this.$emit('post');
+				this.$destroy();
+			}).catch(err => {
+				this.posting = false;
+			});
+		},
 		cancel() {
 			this.$emit('cancel');
 			this.$destroy();
@@ -167,10 +192,10 @@ export default Vue.extend({
 			margin 8px 0 0 0
 			padding 8px
 
-		> [ref='file']
+		> .file
 			display none
 
-		> [ref='text']
+		> textarea
 			display block
 			padding 12px
 			margin 0
@@ -187,8 +212,8 @@ export default Vue.extend({
 			&:disabled
 				opacity 0.5
 
-		> [ref='upload']
-		> [ref='drive']
+		> .upload
+		> .drive
 		.kao
 		.poll
 			display inline-block
diff --git a/src/web/app/mobile/views/components/posts-post-sub.vue b/src/web/app/mobile/views/components/posts-post-sub.vue
deleted file mode 100644
index 421d51b92..000000000
--- a/src/web/app/mobile/views/components/posts-post-sub.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<template>
-<div class="mk-posts-post-sub">
-	<article>
-		<a class="avatar-anchor" href={ '/' + post.user.username }>
-			<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=96' } alt="avatar"/>
-		</a>
-		<div class="main">
-			<header>
-				<a class="name" href={ '/' + post.user.username }>{ post.user.name }</a>
-				<span class="username">@{ post.user.username }</span>
-				<a class="created-at" href={ '/' + post.user.username + '/' + post.id }>
-					<mk-time time={ post.created_at }/>
-				</a>
-			</header>
-			<div class="body">
-				<mk-sub-post-content class="text" post={ post }/>
-			</div>
-		</div>
-	</article>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	props: ['post']
-});
-</script>
-
-
-<style lang="stylus" scoped>
-.mk-posts-post-sub
-	font-size 0.9em
-
-	> article
-		padding 16px
-
-		&:after
-			content ""
-			display block
-			clear both
-
-		&:hover
-			> .main > footer > button
-				color #888
-
-		> .avatar-anchor
-			display block
-			float left
-			margin 0 10px 0 0
-
-			@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
-
-		> .main
-			float left
-			width calc(100% - 54px)
-
-			@media (min-width 500px)
-				width calc(100% - 68px)
-
-			> header
-				display flex
-				margin-bottom 2px
-				white-space nowrap
-
-				> .name
-					display block
-					margin 0 0.5em 0 0
-					padding 0
-					overflow hidden
-					color #607073
-					font-size 1em
-					font-weight 700
-					text-align left
-					text-decoration none
-					text-overflow ellipsis
-
-					&:hover
-						text-decoration underline
-
-				> .username
-					text-align left
-					margin 0
-					color #d1d8da
-
-				> .created-at
-					margin-left auto
-					color #b2b8bb
-
-			> .body
-
-				> .text
-					cursor default
-					margin 0
-					padding 0
-					font-size 1.1em
-					color #717171
-
-					pre
-						max-height 120px
-						font-size 80%
-
-</style>
-
diff --git a/src/web/app/mobile/views/components/posts.post.sub.vue b/src/web/app/mobile/views/components/posts.post.sub.vue
new file mode 100644
index 000000000..5bb6444a6
--- /dev/null
+++ b/src/web/app/mobile/views/components/posts.post.sub.vue
@@ -0,0 +1,108 @@
+<template>
+<div class="sub">
+	<router-link class="avatar-anchor" :to="`/${post.user.username}`">
+		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=96`" alt="avatar"/>
+	</router-link>
+	<div class="main">
+		<header>
+			<router-link class="name" :to="`/${post.user.username}`">{{ post.user.name }}</router-link>
+			<span class="username">@{{ post.user.username }}</span>
+			<router-link class="created-at" :href="`/${post.user.username}/${post.id}`">
+				<mk-time :time="post.created_at"/>
+			</router-link>
+		</header>
+		<div class="body">
+			<mk-sub-post-content class="text" :post="post"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['post']
+});
+</script>
+
+<style lang="stylus" scoped>
+.sub
+	font-size 0.9em
+	padding 16px
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	> .avatar-anchor
+		display block
+		float left
+		margin 0 10px 0 0
+
+		@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
+
+	> .main
+		float left
+		width calc(100% - 54px)
+
+		@media (min-width 500px)
+			width calc(100% - 68px)
+
+		> header
+			display flex
+			margin-bottom 2px
+			white-space nowrap
+
+			> .name
+				display block
+				margin 0 0.5em 0 0
+				padding 0
+				overflow hidden
+				color #607073
+				font-size 1em
+				font-weight 700
+				text-align left
+				text-decoration none
+				text-overflow ellipsis
+
+				&:hover
+					text-decoration underline
+
+			> .username
+				text-align left
+				margin 0
+				color #d1d8da
+
+			> .created-at
+				margin-left auto
+				color #b2b8bb
+
+		> .body
+
+			> .text
+				cursor default
+				margin 0
+				padding 0
+				font-size 1.1em
+				color #717171
+
+				pre
+					max-height 120px
+					font-size 80%
+
+</style>
+
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue
index 225a530b5..9a7d633d4 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -69,8 +69,14 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import MkPostMenu from '../../../common/views/components/post-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './posts.post.sub.vue';
 
 export default Vue.extend({
+	components: {
+		XSub
+	},
 	props: ['post'],
 	data() {
 		return {
@@ -152,6 +158,34 @@ export default Vue.extend({
 				this.$emit('update:post', post);
 			}
 		},
+		reply() {
+			(this as any).apis.post({
+				reply: this.p
+			});
+		},
+		repost() {
+			(this as any).apis.post({
+				repost: this.p
+			});
+		},
+		react() {
+			document.body.appendChild(new MkReactionPicker({
+				propsData: {
+					source: this.$refs.reactButton,
+					post: this.p,
+					compact: true
+				}
+			}).$mount().$el);
+		},
+		menu() {
+			document.body.appendChild(new MkPostMenu({
+				propsData: {
+					source: this.$refs.menuButton,
+					post: this.p,
+					compact: true
+				}
+			}).$mount().$el);
+		}
 	}
 });
 </script>
diff --git a/src/web/app/mobile/views/components/timeline.vue b/src/web/app/mobile/views/components/timeline.vue
index 80fda7560..13f597360 100644
--- a/src/web/app/mobile/views/components/timeline.vue
+++ b/src/web/app/mobile/views/components/timeline.vue
@@ -9,7 +9,7 @@
 			%fa:R comments%
 			%i18n:mobile.tags.mk-home-timeline.empty-timeline%
 		</div>
-		<button v-if="canFetchMore" @click="more" :disabled="fetching" slot="tail">
+		<button @click="more" :disabled="fetching" slot="tail">
 			<span v-if="!fetching">%i18n:mobile.tags.mk-timeline.load-more%</span>
 			<span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span>
 		</button>
diff --git a/src/web/app/mobile/views/components/ui.header.vue b/src/web/app/mobile/views/components/ui.header.vue
index b9b7b4771..2df5ea162 100644
--- a/src/web/app/mobile/views/components/ui.header.vue
+++ b/src/web/app/mobile/views/components/ui.header.vue
@@ -24,7 +24,6 @@ export default Vue.extend({
 	props: ['func'],
 	data() {
 		return {
-			func: null,
 			hasUnreadNotifications: false,
 			hasUnreadMessagingMessages: false,
 			connection: null,
diff --git a/src/web/app/mobile/views/components/ui.nav.vue b/src/web/app/mobile/views/components/ui.nav.vue
index 3796b2765..5ca7e2e94 100644
--- a/src/web/app/mobile/views/components/ui.nav.vue
+++ b/src/web/app/mobile/views/components/ui.nav.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="nav" :style="{ display: isOpen ? 'block' : 'none' }">
-	<div class="backdrop" @click="parent.toggleDrawer"></div>
+	<div class="backdrop" @click="$parent.isDrawerOpening = false"></div>
 	<div class="body">
 		<router-link class="me" v-if="os.isSignedIn" :to="`/${os.i.username}`">
 			<img class="avatar" :src="`${os.i.avatar_url}?thumbnail&size=128`" alt="avatar"/>
@@ -8,36 +8,40 @@
 		</router-link>
 		<div class="links">
 			<ul>
-				<li><router-link href="/">%fa:home%%i18n:mobile.tags.mk-ui-nav.home%%fa:angle-right%</router-link></li>
-				<li><router-link href="/i/notifications">%fa:R bell%%i18n:mobile.tags.mk-ui-nav.notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</router-link></li>
-				<li><router-link href="/i/messaging">%fa:R comments%%i18n:mobile.tags.mk-ui-nav.messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</router-link></li>
+				<li><router-link to="/">%fa:home%%i18n:mobile.tags.mk-ui-nav.home%%fa:angle-right%</router-link></li>
+				<li><router-link to="/i/notifications">%fa:R bell%%i18n:mobile.tags.mk-ui-nav.notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</router-link></li>
+				<li><router-link to="/i/messaging">%fa:R comments%%i18n:mobile.tags.mk-ui-nav.messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</router-link></li>
 			</ul>
 			<ul>
 				<li><a :href="chUrl" target="_blank">%fa:tv%%i18n:mobile.tags.mk-ui-nav.ch%%fa:angle-right%</a></li>
-				<li><router-link href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-ui-nav.drive%%fa:angle-right%</router-link></li>
+				<li><router-link to="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-ui-nav.drive%%fa:angle-right%</router-link></li>
 			</ul>
 			<ul>
 				<li><a @click="search">%fa:search%%i18n:mobile.tags.mk-ui-nav.search%%fa:angle-right%</a></li>
 			</ul>
 			<ul>
-				<li><router-link href="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</router-link></li>
+				<li><router-link to="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</router-link></li>
 			</ul>
 		</div>
-		<a :href="aboutUrl"><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
+		<a :href="docsUrl"><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
 	</div>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import { docsUrl, chUrl } from '../../../config';
 
 export default Vue.extend({
+	props: ['isOpen'],
 	data() {
 		return {
 			hasUnreadNotifications: false,
 			hasUnreadMessagingMessages: false,
 			connection: null,
-			connectionId: null
+			connectionId: null,
+			docsUrl,
+			chUrl
 		};
 	},
 	mounted() {

From 463a38d606a79d5f2382d08c1b5b85b4264b7413 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 17:06:19 +0900
Subject: [PATCH 263/286] wip

---
 src/web/app/mobile/views/components/index.ts |  4 +++
 src/web/app/mobile/views/pages/user.vue      | 37 ++++++++++++--------
 2 files changed, 26 insertions(+), 15 deletions(-)

diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index 715e291a7..7cb9aa4a5 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -6,6 +6,8 @@ import timeline from './timeline.vue';
 import posts from './posts.vue';
 import imagesImage from './images-image.vue';
 import drive from './drive.vue';
+import postPreview from './post-preview.vue';
+import subPostContent from './sub-post-content.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-home', home);
@@ -13,3 +15,5 @@ Vue.component('mk-timeline', timeline);
 Vue.component('mk-posts', posts);
 Vue.component('mk-images-image', imagesImage);
 Vue.component('mk-drive', drive);
+Vue.component('mk-post-preview', postPreview);
+Vue.component('mk-sub-post-content', subPostContent);
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index 2d1611726..afd7e990a 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-ui :func="fn">
-	<span slot="header" v-if="!fetching">%fa:user% {{user.name}}</span>
+	<span slot="header" v-if="!fetching">%fa:user% {{ user.name }}</span>
 	<template slot="funcIcon">%fa:pencil-alt%</template>
 	<div v-if="!fetching" :class="$style.user">
 		<header>
@@ -58,15 +58,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
-const age = require('s-age');
+import age from 's-age';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
 	props: {
-		username: {
-			type: String,
-			required: true
-		},
 		page: {
 			default: 'home'
 		}
@@ -82,19 +78,30 @@ export default Vue.extend({
 			return age(this.user.profile.birthday);
 		}
 	},
+	created() {
+		this.fetch();
+	},
+	watch: {
+		$route: 'fetch'
+	},
 	mounted() {
 		document.documentElement.style.background = '#313a42';
-		Progress.start();
-
 		(this as any).api('users/show', {
-			username: this.username
-		}).then(user => {
-			this.user = user;
-			this.fetching = false;
+	},
+	methods: {
+		fetch() {
+			Progress.start();
 
-			Progress.done();
-			document.title = user.name + ' | Misskey';
-		});
+			(this as any).api('users/show', {
+				username: this.$route.params.user
+			}).then(user => {
+				this.user = user;
+				this.fetching = false;
+
+				Progress.done();
+				document.title = user.name + ' | Misskey';
+			});
+		}
 	}
 });
 </script>

From 47efc83f16fbd69f432e585018ee03e3ff1a77f1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 17:32:58 +0900
Subject: [PATCH 264/286] wip

---
 src/web/app/mobile/views/components/index.ts  |  8 ++
 ...ost-detail-sub.vue => post-detail.sub.vue} | 23 +++--
 .../mobile/views/components/post-detail.vue   | 95 +++++++++++++------
 src/web/app/mobile/views/pages/user.vue       | 19 ++--
 .../{home-activity.vue => home.activity.vue}  |  8 +-
 ...u-know.vue => home.followers-you-know.vue} |  4 +-
 .../{home-friends.vue => home.friends.vue}    |  8 +-
 .../user/{home-photos.vue => home.photos.vue} |  4 +-
 .../user/{home-posts.vue => home.posts.vue}   | 12 +--
 src/web/app/mobile/views/pages/user/home.vue  | 39 ++++----
 10 files changed, 133 insertions(+), 87 deletions(-)
 rename src/web/app/mobile/views/components/{post-detail-sub.vue => post-detail.sub.vue} (69%)
 rename src/web/app/mobile/views/pages/user/{home-activity.vue => home.activity.vue} (91%)
 rename src/web/app/mobile/views/pages/user/{followers-you-know.vue => home.followers-you-know.vue} (93%)
 rename src/web/app/mobile/views/pages/user/{home-friends.vue => home.friends.vue} (80%)
 rename src/web/app/mobile/views/pages/user/{home-photos.vue => home.photos.vue} (96%)
 rename src/web/app/mobile/views/pages/user/{home-posts.vue => home.posts.vue} (66%)

diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index 7cb9aa4a5..739bfda17 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -8,6 +8,10 @@ import imagesImage from './images-image.vue';
 import drive from './drive.vue';
 import postPreview from './post-preview.vue';
 import subPostContent from './sub-post-content.vue';
+import postCard from './post-card.vue';
+import userCard from './user-card.vue';
+import postDetail from './post-detail.vue';
+import followButton from './follow-button.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-home', home);
@@ -17,3 +21,7 @@ Vue.component('mk-images-image', imagesImage);
 Vue.component('mk-drive', drive);
 Vue.component('mk-post-preview', postPreview);
 Vue.component('mk-sub-post-content', subPostContent);
+Vue.component('mk-post-card', postCard);
+Vue.component('mk-user-card', userCard);
+Vue.component('mk-post-detail', postDetail);
+Vue.component('mk-follow-button', followButton);
diff --git a/src/web/app/mobile/views/components/post-detail-sub.vue b/src/web/app/mobile/views/components/post-detail.sub.vue
similarity index 69%
rename from src/web/app/mobile/views/components/post-detail-sub.vue
rename to src/web/app/mobile/views/components/post-detail.sub.vue
index 8836bb1b3..dff0cef51 100644
--- a/src/web/app/mobile/views/components/post-detail-sub.vue
+++ b/src/web/app/mobile/views/components/post-detail.sub.vue
@@ -1,18 +1,18 @@
 <template>
-<div class="mk-post-detail-sub">
-	<a class="avatar-anchor" href={ '/' + post.user.username }>
-		<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-	</a>
+<div class="root sub">
+	<router-link class="avatar-anchor" :to="`/${post.user.username}`">
+		<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+	</router-link>
 	<div class="main">
 		<header>
-			<a class="name" href={ '/' + post.user.username }>{ post.user.name }</a>
-			<span class="username">@{ post.user.username }</span>
-			<a class="time" href={ '/' + post.user.username + '/' + post.id }>
-				<mk-time time={ post.created_at }/>
-			</a>
+			<router-link class="name" :to="`/${post.user.username}`">{{ post.user.name }}</router-link>
+			<span class="username">@{{ post.user.username }}</span>
+			<router-link class="time" :to="`/${post.user.username}/${post.id}`">
+				<mk-time :time="post.created_at"/>
+			</router-link>
 		</header>
 		<div class="body">
-			<mk-sub-post-content class="text" post={ post }/>
+			<mk-sub-post-content class="text" :post="post"/>
 		</div>
 	</div>
 </div>
@@ -26,8 +26,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-post-detail-sub
-	margin 0
+.root.sub
 	padding 8px
 	font-size 0.9em
 	background #fdfdfd
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index 87a591ff6..76057525f 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -1,58 +1,63 @@
 <template>
 <div class="mk-post-detail">
-	<button class="read-more" v-if="p.reply && p.reply.reply_id && context == null" @click="loadContext" disabled={ loadingContext }>
+	<button
+		class="more"
+		v-if="p.reply && p.reply.reply_id && context == null"
+		@click="fetchContext"
+		:disabled="fetchingContext"
+	>
 		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
 		<template v-if="contextFetching">%fa:spinner .pulse%</template>
 	</button>
 	<div class="context">
-		<template each={ post in context }>
-			<mk-post-detail-sub post={ post }/>
-		</template>
+		<x-sub v-for="post in context" :key="post.id" :post="post"/>
 	</div>
 	<div class="reply-to" v-if="p.reply">
-		<mk-post-detail-sub post={ p.reply }/>
+		<x-sub :post="p.reply"/>
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
-			<a class="avatar-anchor" href={ '/' + post.user.username }>
-				<img class="avatar" src={ post.user.avatar_url + '?thumbnail&size=32' } alt="avatar"/></a>
-				%fa:retweet%<a class="name" href={ '/' + post.user.username }>
-				{ post.user.name }
-			</a>
+			<router-link class="avatar-anchor" :to="`/${post.user.username}`">
+				<img class="avatar" :src="`${post.user.avatar_url}?thumbnail&size=32`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%
+			<router-link class="name" :to="`/${post.user.username}`">
+				{{ post.user.name }}
+			</router-link>
 			がRepost
 		</p>
 	</div>
 	<article>
 		<header>
-			<a class="avatar-anchor" href={ '/' + p.user.username }>
-				<img class="avatar" src={ p.user.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
-			</a>
+			<router-link class="avatar-anchor" :to="`/${p.user.username}`">
+				<img class="avatar" :src="`${p.user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
+			</router-link>
 			<div>
-				<a class="name" href={ '/' + p.user.username }>{ p.user.name }</a>
-				<span class="username">@{ p.user.username }</span>
+				<router-link class="name" :to="`/${p.user.username}`">{{ p.user.name }}</router-link>
+				<span class="username">@{{ p.user.username }}</span>
 			</div>
 		</header>
 		<div class="body">
 			<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i"/>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			<div class="media" v-if="p.media">
-				<mk-images images={ p.media }/>
+				<mk-images :images="p.media"/>
 			</div>
-			<mk-poll v-if="p.poll" post={ p }/>
+			<mk-poll v-if="p.poll" :post="p"/>
 		</div>
-		<a class="time" href={ '/' + p.user.username + '/' + p.id }>
-			<mk-time time={ p.created_at } mode="detail"/>
-		</a>
+		<router-link class="time" :to="`/${p.user.username}/${p.id}`">
+			<mk-time :time="p.created_at" mode="detail"/>
+		</router-link>
 		<footer>
-			<mk-reactions-viewer post={ p }/>
+			<mk-reactions-viewer :post="p"/>
 			<button @click="reply" title="%i18n:mobile.tags.mk-post-detail.reply%">
-				%fa:reply%<p class="count" v-if="p.replies_count > 0">{ p.replies_count }</p>
+				%fa:reply%<p class="count" v-if="p.replies_count > 0">{{ p.replies_count }}</p>
 			</button>
 			<button @click="repost" title="Repost">
-				%fa:retweet%<p class="count" v-if="p.repost_count > 0">{ p.repost_count }</p>
+				%fa:retweet%<p class="count" v-if="p.repost_count > 0">{{ p.repost_count }}</p>
 			</button>
 			<button :class="{ reacted: p.my_reaction != null }" @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-post-detail.reaction%">
-				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{ p.reactions_count }</p>
+				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
 			</button>
 			<button @click="menu" ref="menuButton">
 				%fa:ellipsis-h%
@@ -60,19 +65,21 @@
 		</footer>
 	</article>
 	<div class="replies" v-if="!compact">
-		<template each={ post in replies }>
-			<mk-post-detail-sub post={ post }/>
-		</template>
+		<x-sub v-for="post in replies" :key="post.id" :post="post"/>
 	</div>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import getPostSummary from '../../../../common/get-post-summary.ts';
-import openPostForm from '../scripts/open-post-form';
+import MkPostMenu from '../../../common/views/components/post-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './post-detail.sub.vue';
 
 export default Vue.extend({
+	components: {
+		XSub
+	},
 	props: {
 		post: {
 			type: Object,
@@ -135,6 +142,34 @@ export default Vue.extend({
 				this.contextFetching = false;
 				this.context = context.reverse();
 			});
+		},
+		reply() {
+			(this as any).apis.post({
+				reply: this.p
+			});
+		},
+		repost() {
+			(this as any).apis.post({
+				repost: this.p
+			});
+		},
+		react() {
+			document.body.appendChild(new MkReactionPicker({
+				propsData: {
+					source: this.$refs.reactButton,
+					post: this.p,
+					compact: true
+				}
+			}).$mount().$el);
+		},
+		menu() {
+			document.body.appendChild(new MkPostMenu({
+				propsData: {
+					source: this.$refs.menuButton,
+					post: this.p,
+					compact: true
+				}
+			}).$mount().$el);
 		}
 	}
 });
@@ -154,7 +189,7 @@ export default Vue.extend({
 	> .fetching
 		padding 64px 0
 
-	> .read-more
+	> .more
 		display block
 		margin 0
 		padding 10px 0
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index afd7e990a..b76f0ac84 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -1,8 +1,8 @@
 <template>
-<mk-ui :func="fn">
+<mk-ui>
 	<span slot="header" v-if="!fetching">%fa:user% {{ user.name }}</span>
 	<template slot="funcIcon">%fa:pencil-alt%</template>
-	<div v-if="!fetching" :class="$style.user">
+	<main v-if="!fetching">
 		<header>
 			<div class="banner" :style="user.banner_url ? `background-image: url(${user.banner_url}?thumbnail&size=1024)` : ''"></div>
 			<div class="body">
@@ -48,11 +48,11 @@
 			</nav>
 		</header>
 		<div class="body">
-			<mk-user-home v-if="page == 'home'" :user="user"/>
+			<x-home v-if="page == 'home'" :user="user"/>
 			<mk-user-timeline v-if="page == 'posts'" :user="user"/>
 			<mk-user-timeline v-if="page == 'media'" :user="user" with-media/>
 		</div>
-	</div>
+	</main>
 </mk-ui>
 </template>
 
@@ -60,8 +60,12 @@
 import Vue from 'vue';
 import age from 's-age';
 import Progress from '../../../common/scripts/loading';
+import XHome from './user/home.vue';
 
 export default Vue.extend({
+	components: {
+		XHome
+	},
 	props: {
 		page: {
 			default: 'home'
@@ -86,7 +90,6 @@ export default Vue.extend({
 	},
 	mounted() {
 		document.documentElement.style.background = '#313a42';
-		(this as any).api('users/show', {
 	},
 	methods: {
 		fetch() {
@@ -106,8 +109,8 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" module>
-.user
+<style lang="stylus" scoped>
+main
 	> header
 		box-shadow 0 4px 4px rgba(0, 0, 0, 0.3)
 
@@ -140,7 +143,7 @@ export default Vue.extend({
 						left -2px
 						bottom -2px
 						width 100%
-						border 2px solid #313a42
+						border 3px solid #313a42
 						border-radius 6px
 
 						@media (min-width 500px)
diff --git a/src/web/app/mobile/views/pages/user/home-activity.vue b/src/web/app/mobile/views/pages/user/home.activity.vue
similarity index 91%
rename from src/web/app/mobile/views/pages/user/home-activity.vue
rename to src/web/app/mobile/views/pages/user/home.activity.vue
index 87c1dca89..87970795b 100644
--- a/src/web/app/mobile/views/pages/user/home-activity.vue
+++ b/src/web/app/mobile/views/pages/user/home.activity.vue
@@ -1,7 +1,7 @@
 <template>
-<div class="mk-user-home-activity">
+<div class="root activity">
 	<svg v-if="data" ref="canvas" viewBox="0 0 30 1" preserveAspectRatio="none">
-		<g v-for="(d, i) in data.reverse()">
+		<g v-for="(d, i) in data">
 			<rect width="0.8" :height="d.postsH"
 				:x="i + 0.1" :y="1 - d.postsH - d.repliesH - d.repostsH"
 				fill="#41ddde"/>
@@ -39,6 +39,7 @@ export default Vue.extend({
 				d.repliesH = d.replies / this.peak;
 				d.repostsH = d.reposts / this.peak;
 			});
+			data.reverse();
 			this.data = data;
 		});
 	}
@@ -46,8 +47,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-user-home-activity
-	display block
+.root.activity
 	max-width 600px
 	margin 0 auto
 
diff --git a/src/web/app/mobile/views/pages/user/followers-you-know.vue b/src/web/app/mobile/views/pages/user/home.followers-you-know.vue
similarity index 93%
rename from src/web/app/mobile/views/pages/user/followers-you-know.vue
rename to src/web/app/mobile/views/pages/user/home.followers-you-know.vue
index eb0ff68bd..acefcaa10 100644
--- a/src/web/app/mobile/views/pages/user/followers-you-know.vue
+++ b/src/web/app/mobile/views/pages/user/home.followers-you-know.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-user-home-followers-you-know">
+<div class="root followers-you-know">
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p>
 	<div v-if="!fetching && users.length > 0">
 		<a v-for="user in users" :key="user.id" :href="`/${user.username}`">
@@ -34,7 +34,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-user-home-followers-you-know
+.root.followers-you-know
 
 	> div
 		padding 4px
diff --git a/src/web/app/mobile/views/pages/user/home-friends.vue b/src/web/app/mobile/views/pages/user/home.friends.vue
similarity index 80%
rename from src/web/app/mobile/views/pages/user/home-friends.vue
rename to src/web/app/mobile/views/pages/user/home.friends.vue
index 543ed9b30..b37f1a2fe 100644
--- a/src/web/app/mobile/views/pages/user/home-friends.vue
+++ b/src/web/app/mobile/views/pages/user/home.friends.vue
@@ -1,6 +1,6 @@
 <template>
-<div class="mk-user-home-friends">
-	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-frequently-replied-users.loading%<mk-ellipsis/></p>
+<div class="root friends">
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-frequently-replied-users.loading%<mk-ellipsis/></p>
 	<div v-if="!fetching && users.length > 0">
 		<mk-user-card v-for="user in users" :key="user.id" :user="user"/>
 	</div>
@@ -30,7 +30,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-user-home-friends
+.root.friends
 	> div
 		overflow-x scroll
 		-webkit-overflow-scrolling touch
@@ -41,7 +41,7 @@ export default Vue.extend({
 			&:not(:last-child)
 				margin-right 8px
 
-	> .initializing
+	> .fetching
 	> .empty
 		margin 0
 		padding 16px
diff --git a/src/web/app/mobile/views/pages/user/home-photos.vue b/src/web/app/mobile/views/pages/user/home.photos.vue
similarity index 96%
rename from src/web/app/mobile/views/pages/user/home-photos.vue
rename to src/web/app/mobile/views/pages/user/home.photos.vue
index dbb2a410a..2a6343189 100644
--- a/src/web/app/mobile/views/pages/user/home-photos.vue
+++ b/src/web/app/mobile/views/pages/user/home.photos.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-user-home-photos">
+<div class="root photos">
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-photos.loading%<mk-ellipsis/></p>
 	<div class="stream" v-if="!fetching && images.length > 0">
 		<a v-for="image in images" :key="image.id"
@@ -43,7 +43,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-user-home-photos
+.root.photos
 
 	> .stream
 		display -webkit-flex
diff --git a/src/web/app/mobile/views/pages/user/home-posts.vue b/src/web/app/mobile/views/pages/user/home.posts.vue
similarity index 66%
rename from src/web/app/mobile/views/pages/user/home-posts.vue
rename to src/web/app/mobile/views/pages/user/home.posts.vue
index 8b1ea2de5..70b20ce94 100644
--- a/src/web/app/mobile/views/pages/user/home-posts.vue
+++ b/src/web/app/mobile/views/pages/user/home.posts.vue
@@ -1,10 +1,10 @@
 <template>
-<div class="mk-user-home-posts">
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-posts.loading%<mk-ellipsis/></p>
-	<div v-if="!initializing && posts.length > 0">
+<div class="root posts">
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-posts.loading%<mk-ellipsis/></p>
+	<div v-if="!fetching && posts.length > 0">
 		<mk-post-card v-for="post in posts" :key="post.id" :post="post"/>
 	</div>
-	<p class="empty" v-if="!initializing && posts.length == 0">%i18n:mobile.tags.mk-user-overview-posts.no-posts%</p>
+	<p class="empty" v-if="!fetching && posts.length == 0">%i18n:mobile.tags.mk-user-overview-posts.no-posts%</p>
 </div>
 </template>
 
@@ -30,7 +30,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-user-home-posts
+.root.posts
 
 	> div
 		overflow-x scroll
@@ -44,7 +44,7 @@ export default Vue.extend({
 			&:not(:last-child)
 				margin-right 8px
 
-	> .initializing
+	> .fetching
 	> .empty
 		margin 0
 		padding 16px
diff --git a/src/web/app/mobile/views/pages/user/home.vue b/src/web/app/mobile/views/pages/user/home.vue
index 44ddd54dc..040b916ca 100644
--- a/src/web/app/mobile/views/pages/user/home.vue
+++ b/src/web/app/mobile/views/pages/user/home.vue
@@ -1,46 +1,34 @@
 <template>
-<div class="mk-user-home">
+<div class="root home">
 	<mk-post-detail v-if="user.pinned_post" :post="user.pinned_post" compact/>
 	<section class="recent-posts">
 		<h2>%fa:R comments%%i18n:mobile.tags.mk-user-overview.recent-posts%</h2>
 		<div>
-			<mk-user-home-posts :user="user"/>
+			<x-posts :user="user"/>
 		</div>
 	</section>
 	<section class="images">
 		<h2>%fa:image%%i18n:mobile.tags.mk-user-overview.images%</h2>
 		<div>
-			<mk-user-home-photos :user="user"/>
+			<x-photos :user="user"/>
 		</div>
 	</section>
 	<section class="activity">
 		<h2>%fa:chart-bar%%i18n:mobile.tags.mk-user-overview.activity%</h2>
 		<div>
-			<mk-user-home-activity-chart :user="user"/>
-		</div>
-	</section>
-	<section class="keywords">
-		<h2>%fa:R comment%%i18n:mobile.tags.mk-user-overview.keywords%</h2>
-		<div>
-			<mk-user-home-keywords :user="user"/>
-		</div>
-	</section>
-	<section class="domains">
-		<h2>%fa:globe%%i18n:mobile.tags.mk-user-overview.domains%</h2>
-		<div>
-			<mk-user-home-domains :user="user"/>
+			<x-activity :user="user"/>
 		</div>
 	</section>
 	<section class="frequently-replied-users">
 		<h2>%fa:users%%i18n:mobile.tags.mk-user-overview.frequently-replied-users%</h2>
 		<div>
-			<mk-user-home-frequently-replied-users :user="user"/>
+			<x-friends :user="user"/>
 		</div>
 	</section>
 	<section class="followers-you-know" v-if="os.isSignedIn && os.i.id !== user.id">
 		<h2>%fa:users%%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2>
 		<div>
-			<mk-user-home-followers-you-know :user="user"/>
+			<x-followers-you-know :user="user"/>
 		</div>
 	</section>
 	<p>%i18n:mobile.tags.mk-user-overview.last-used-at%: <b><mk-time :time="user.last_used_at"/></b></p>
@@ -49,13 +37,26 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import XPosts from './home.posts.vue';
+import XPhotos from './home.photos.vue';
+import XFriends from './home.friends.vue';
+import XFollowersYouKnow from './home.followers-you-know.vue';
+import XActivity from './home.activity.vue';
+
 export default Vue.extend({
+	components: {
+		XPosts,
+		XPhotos,
+		XFriends,
+		XFollowersYouKnow,
+		XActivity
+	},
 	props: ['user']
 });
 </script>
 
 <style lang="stylus" scoped>
-.mk-user-home
+.root.home
 	max-width 600px
 	margin 0 auto
 

From 7ce857593a0e618f6f7b4509b80b4f3e989dc389 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 17:37:40 +0900
Subject: [PATCH 265/286] wip

---
 src/web/app/mobile/views/components/posts.vue    | 4 +++-
 src/web/app/mobile/views/components/timeline.vue | 2 +-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/web/app/mobile/views/components/posts.vue b/src/web/app/mobile/views/components/posts.vue
index 01897eafd..48ed01d0a 100644
--- a/src/web/app/mobile/views/components/posts.vue
+++ b/src/web/app/mobile/views/components/posts.vue
@@ -8,7 +8,9 @@
 			<span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span>
 		</p>
 	</template>
-	<slot name="tail"></slot>
+	<footer>
+		<slot name="tail"></slot>
+	</footer>
 </div>
 </template>
 
diff --git a/src/web/app/mobile/views/components/timeline.vue b/src/web/app/mobile/views/components/timeline.vue
index 13f597360..2354f8a7a 100644
--- a/src/web/app/mobile/views/components/timeline.vue
+++ b/src/web/app/mobile/views/components/timeline.vue
@@ -74,8 +74,8 @@ export default Vue.extend({
 			(this as any).api('posts/timeline', {
 				until_id: this.posts[this.posts.length - 1].id
 			}).then(posts => {
+				this.posts = this.posts.concat(posts);
 				this.moreFetching = false;
-				this.posts.unshift(posts);
 			});
 		},
 		onPost(post) {

From fcf77dc09556a66cc3541af9aecbe1f08965b359 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 17:38:48 +0900
Subject: [PATCH 266/286] wip

---
 src/web/app/mobile/views/components/timeline.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/web/app/mobile/views/components/timeline.vue b/src/web/app/mobile/views/components/timeline.vue
index 2354f8a7a..dc0f2ae1b 100644
--- a/src/web/app/mobile/views/components/timeline.vue
+++ b/src/web/app/mobile/views/components/timeline.vue
@@ -65,6 +65,7 @@ export default Vue.extend({
 			}).then(posts => {
 				this.posts = posts;
 				this.fetching = false;
+				this.$emit('loaded');
 				if (cb) cb();
 			});
 		},

From f2fbf7f818ef244008cf8cafb3b1baba0496183e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 17:51:08 +0900
Subject: [PATCH 267/286] wip

---
 src/web/app/desktop/views/components/index.ts         | 2 ++
 src/web/app/mobile/views/components/friends-maker.vue | 7 ++++---
 src/web/app/mobile/views/components/index.ts          | 2 ++
 src/web/app/mobile/views/components/posts.vue         | 4 ++++
 src/web/app/mobile/views/components/timeline.vue      | 9 +++++++--
 5 files changed, 19 insertions(+), 5 deletions(-)

diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 3bcfc2fdd..0e4629172 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -26,6 +26,7 @@ import postDetail from './post-detail.vue';
 import settings from './settings.vue';
 import calendar from './calendar.vue';
 import activity from './activity.vue';
+import friendsMaker from './friends-maker.vue';
 import wNav from './widgets/nav.vue';
 import wCalendar from './widgets/calendar.vue';
 import wPhotoStream from './widgets/photo-stream.vue';
@@ -74,6 +75,7 @@ Vue.component('mk-post-detail', postDetail);
 Vue.component('mk-settings', settings);
 Vue.component('mk-calendar', calendar);
 Vue.component('mk-activity', activity);
+Vue.component('mk-friends-maker', friendsMaker);
 Vue.component('mkw-nav', wNav);
 Vue.component('mkw-calendar', wCalendar);
 Vue.component('mkw-photo-stream', wPhotoStream);
diff --git a/src/web/app/mobile/views/components/friends-maker.vue b/src/web/app/mobile/views/components/friends-maker.vue
index 8e7bf2d63..961a5f568 100644
--- a/src/web/app/mobile/views/components/friends-maker.vue
+++ b/src/web/app/mobile/views/components/friends-maker.vue
@@ -2,9 +2,7 @@
 <div class="mk-friends-maker">
 	<p class="title">気になるユーザーをフォロー:</p>
 	<div class="users" v-if="!fetching && users.length > 0">
-		<template each={ users }>
-			<mk-user-card user={ this } />
-		</template>
+		<mk-user-card v-for="user in users" :key="user.id" :user="user"/>
 	</div>
 	<p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p>
 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
@@ -47,6 +45,9 @@ export default Vue.extend({
 				this.page++;
 			}
 			this.fetch();
+		},
+		close() {
+			this.$destroy();
 		}
 	}
 });
diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index 739bfda17..f2c8ddf3e 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -12,6 +12,7 @@ import postCard from './post-card.vue';
 import userCard from './user-card.vue';
 import postDetail from './post-detail.vue';
 import followButton from './follow-button.vue';
+import friendsMaker from './friends-maker.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-home', home);
@@ -25,3 +26,4 @@ Vue.component('mk-post-card', postCard);
 Vue.component('mk-user-card', userCard);
 Vue.component('mk-post-detail', postDetail);
 Vue.component('mk-follow-button', followButton);
+Vue.component('mk-friends-maker', friendsMaker);
diff --git a/src/web/app/mobile/views/components/posts.vue b/src/web/app/mobile/views/components/posts.vue
index 48ed01d0a..b028264b5 100644
--- a/src/web/app/mobile/views/components/posts.vue
+++ b/src/web/app/mobile/views/components/posts.vue
@@ -1,6 +1,7 @@
 <template>
 <div class="mk-posts">
 	<slot name="head"></slot>
+	<slot></slot>
 	<template v-for="(post, i) in _posts">
 		<x-post :post="post" :key="post.id"/>
 		<p class="date" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date">
@@ -91,6 +92,9 @@ export default Vue.extend({
 		border-bottom-left-radius 4px
 		border-bottom-right-radius 4px
 
+		&:empty
+			display none
+
 		> button
 			margin 0
 			padding 16px
diff --git a/src/web/app/mobile/views/components/timeline.vue b/src/web/app/mobile/views/components/timeline.vue
index dc0f2ae1b..e7a9f2df1 100644
--- a/src/web/app/mobile/views/components/timeline.vue
+++ b/src/web/app/mobile/views/components/timeline.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-timeline">
 	<mk-friends-maker v-if="alone"/>
-	<mk-posts ref="timeline" :posts="posts">
+	<mk-posts :posts="posts">
 		<div class="init" v-if="fetching">
 			%fa:spinner .pulse%%i18n:common.loading%
 		</div>
@@ -9,7 +9,7 @@
 			%fa:R comments%
 			%i18n:mobile.tags.mk-home-timeline.empty-timeline%
 		</div>
-		<button @click="more" :disabled="fetching" slot="tail">
+		<button v-if="!fetching && posts.length != 0" @click="more" :disabled="fetching" slot="tail">
 			<span v-if="!fetching">%i18n:mobile.tags.mk-timeline.load-more%</span>
 			<span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span>
 		</button>
@@ -88,3 +88,8 @@ export default Vue.extend({
 	}
 });
 </script>
+
+<style lang="stylus" scoped>
+.mk-friends-maker
+	margin-bottom 8px
+</style>

From f64a921575ac10545877d706577eeb978ec55a7d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 17:57:14 +0900
Subject: [PATCH 268/286] wip

---
 src/web/app/mobile/script.ts                          |  2 ++
 src/web/app/mobile/views/components/index.ts          |  2 ++
 src/web/app/mobile/views/components/notifications.vue | 11 ++++++-----
 .../pages/{notification.vue => notifications.vue}     |  3 ++-
 4 files changed, 12 insertions(+), 6 deletions(-)
 rename src/web/app/mobile/views/pages/{notification.vue => notifications.vue} (92%)

diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index a2f118b8f..eef7c20f0 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -19,6 +19,7 @@ import MkSignup from './views/pages/signup.vue';
 import MkUser from './views/pages/user.vue';
 import MkSelectDrive from './views/pages/selectdrive.vue';
 import MkDrive from './views/pages/drive.vue';
+import MkNotifications from './views/pages/notifications.vue';
 
 /**
  * init
@@ -47,6 +48,7 @@ init((launch) => {
 	app.$router.addRoutes([
 		{ path: '/', name: 'index', component: MkIndex },
 		{ path: '/signup', name: 'signup', component: MkSignup },
+		{ path: '/i/notifications', component: MkNotifications },
 		{ path: '/i/drive', component: MkDrive },
 		{ path: '/i/drive/folder/:folder', component: MkDrive },
 		{ path: '/selectdrive', component: MkSelectDrive },
diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index f2c8ddf3e..658cc4863 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -13,6 +13,7 @@ import userCard from './user-card.vue';
 import postDetail from './post-detail.vue';
 import followButton from './follow-button.vue';
 import friendsMaker from './friends-maker.vue';
+import notifications from './notifications.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-home', home);
@@ -27,3 +28,4 @@ Vue.component('mk-user-card', userCard);
 Vue.component('mk-post-detail', postDetail);
 Vue.component('mk-follow-button', followButton);
 Vue.component('mk-friends-maker', friendsMaker);
+Vue.component('mk-notifications', notifications);
diff --git a/src/web/app/mobile/views/components/notifications.vue b/src/web/app/mobile/views/components/notifications.vue
index cc4b743ac..99083ed4b 100644
--- a/src/web/app/mobile/views/components/notifications.vue
+++ b/src/web/app/mobile/views/components/notifications.vue
@@ -4,16 +4,16 @@
 		<template v-for="(notification, i) in _notifications">
 			<mk-notification :notification="notification" :key="notification.id"/>
 			<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date">
-				<span>%fa:angle-up%{ notification._datetext }</span>
-				<span>%fa:angle-down%{ _notifications[i + 1]._datetext }</span>
+				<span>%fa:angle-up%{{ notification._datetext }}</span>
+				<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
 			</p>
 		</template>
 	</div>
-	<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" disabled={ fetchingMoreNotifications }>
-		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{ fetchingMoreNotifications ? '%i18n:common.fetching%' : '%i18n:mobile.tags.mk-notifications.more%' }
+	<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
+		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-notifications.more%' }
 	</button>
 	<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:mobile.tags.mk-notifications.empty%</p>
-	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.fetching%<mk-ellipsis/></p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 </div>
 </template>
 
@@ -59,6 +59,7 @@ export default Vue.extend({
 
 			this.notifications = notifications;
 			this.fetching = false;
+			this.$emit('fetched');
 		});
 	},
 	beforeDestroy() {
diff --git a/src/web/app/mobile/views/pages/notification.vue b/src/web/app/mobile/views/pages/notifications.vue
similarity index 92%
rename from src/web/app/mobile/views/pages/notification.vue
rename to src/web/app/mobile/views/pages/notifications.vue
index 0685bd127..b1243dbc7 100644
--- a/src/web/app/mobile/views/pages/notification.vue
+++ b/src/web/app/mobile/views/pages/notifications.vue
@@ -1,6 +1,7 @@
 <template>
-<mk-ui :func="fn" func-icon="%fa:check%">
+<mk-ui :func="fn">
 	<span slot="header">%fa:R bell%%i18n:mobile.tags.mk-notifications-page.notifications%</span>
+	<span slot="funcIcon">%fa:check%</span>
 	<mk-notifications @fetched="onFetched"/>
 </mk-ui>
 </template>

From eec84157a3d12458c83861318eda705355bfc3cd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 18:06:32 +0900
Subject: [PATCH 269/286] wip

---
 .../components/messaging-room.message.vue     |  4 +--
 src/web/app/mobile/script.ts                  |  4 +++
 src/web/app/mobile/views/components/index.ts  |  2 ++
 .../app/mobile/views/pages/messaging-room.vue | 26 +++++++++++++------
 src/web/app/mobile/views/pages/messaging.vue  |  4 +--
 5 files changed, 28 insertions(+), 12 deletions(-)

diff --git a/src/web/app/common/views/components/messaging-room.message.vue b/src/web/app/common/views/components/messaging-room.message.vue
index 95a6efa28..2464eceb7 100644
--- a/src/web/app/common/views/components/messaging-room.message.vue
+++ b/src/web/app/common/views/components/messaging-room.message.vue
@@ -5,8 +5,8 @@
 	</a>
 	<div class="content-container">
 		<div class="balloon">
-			<p class="read" v-if="message.is_me && message.is_read">%i18n:common.tags.mk-messaging-message.is-read%</p>
-			<button class="delete-button" v-if="message.is_me" title="%i18n:common.delete%">
+			<p class="read" v-if="isMe && message.is_read">%i18n:common.tags.mk-messaging-message.is-read%</p>
+			<button class="delete-button" v-if="isMe" title="%i18n:common.delete%">
 				<img src="/assets/desktop/messaging/delete.png" alt="Delete"/>
 			</button>
 			<div class="content" v-if="!message.is_deleted">
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index eef7c20f0..904cebc7e 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -20,6 +20,8 @@ import MkUser from './views/pages/user.vue';
 import MkSelectDrive from './views/pages/selectdrive.vue';
 import MkDrive from './views/pages/drive.vue';
 import MkNotifications from './views/pages/notifications.vue';
+import MkMessaging from './views/pages/messaging.vue';
+import MkMessagingRoom from './views/pages/messaging-room.vue';
 
 /**
  * init
@@ -49,6 +51,8 @@ init((launch) => {
 		{ path: '/', name: 'index', component: MkIndex },
 		{ path: '/signup', name: 'signup', component: MkSignup },
 		{ path: '/i/notifications', component: MkNotifications },
+		{ path: '/i/messaging', component: MkMessaging },
+		{ path: '/i/messaging/:username', component: MkMessagingRoom },
 		{ path: '/i/drive', component: MkDrive },
 		{ path: '/i/drive/folder/:folder', component: MkDrive },
 		{ path: '/selectdrive', component: MkSelectDrive },
diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index 658cc4863..f5e4ce48f 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -14,6 +14,7 @@ import postDetail from './post-detail.vue';
 import followButton from './follow-button.vue';
 import friendsMaker from './friends-maker.vue';
 import notifications from './notifications.vue';
+import notificationPreview from './notification-preview.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-home', home);
@@ -29,3 +30,4 @@ Vue.component('mk-post-detail', postDetail);
 Vue.component('mk-follow-button', followButton);
 Vue.component('mk-friends-maker', friendsMaker);
 Vue.component('mk-notifications', notifications);
+Vue.component('mk-notification-preview', notificationPreview);
diff --git a/src/web/app/mobile/views/pages/messaging-room.vue b/src/web/app/mobile/views/pages/messaging-room.vue
index 671ede217..a653145c1 100644
--- a/src/web/app/mobile/views/pages/messaging-room.vue
+++ b/src/web/app/mobile/views/pages/messaging-room.vue
@@ -17,15 +17,25 @@ export default Vue.extend({
 			user: null
 		};
 	},
-	mounted() {
-		(this as any).api('users/show', {
-			username: (this as any).$route.params.user
-		}).then(user => {
-			this.user = user;
-			this.fetching = false;
+	watch: {
+		$route: 'fetch'
+	},
+	created() {
+		document.documentElement.style.background = '#fff';
+		this.fetch();
+	},
+	methods: {
+		fetch() {
+			this.fetching = true;
+			(this as any).api('users/show', {
+				username: (this as any).$route.params.username
+			}).then(user => {
+				this.user = user;
+				this.fetching = false;
 
-			document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${user.name} | Misskey`;
-		});
+				document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${user.name} | Misskey`;
+			});
+		}
 	}
 });
 </script>
diff --git a/src/web/app/mobile/views/pages/messaging.vue b/src/web/app/mobile/views/pages/messaging.vue
index 607e44650..f36ad4a4f 100644
--- a/src/web/app/mobile/views/pages/messaging.vue
+++ b/src/web/app/mobile/views/pages/messaging.vue
@@ -9,7 +9,8 @@
 import Vue from 'vue';
 export default Vue.extend({
 	mounted() {
-		document.title = 'Misskey | %i18n:mobile.tags.mk-messaging-page.message%';
+		document.title = 'Misskey %i18n:mobile.tags.mk-messaging-page.message%';
+		document.documentElement.style.background = '#fff';
 	},
 	methods: {
 		navigate(user) {
@@ -18,4 +19,3 @@ export default Vue.extend({
 	}
 });
 </script>
-

From 79698314adb4d356223f579ac1df853adf71e937 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 18:37:47 +0900
Subject: [PATCH 270/286] wip

---
 src/web/app/mobile/script.ts                  |  1 +
 .../views/components/drive.file-detail.vue    |  4 +-
 src/web/app/mobile/views/components/drive.vue |  8 ++--
 src/web/app/mobile/views/pages/drive.vue      | 40 ++++++++++++++-----
 4 files changed, 38 insertions(+), 15 deletions(-)

diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index 904cebc7e..07912a178 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -55,6 +55,7 @@ init((launch) => {
 		{ path: '/i/messaging/:username', component: MkMessagingRoom },
 		{ path: '/i/drive', component: MkDrive },
 		{ path: '/i/drive/folder/:folder', component: MkDrive },
+		{ path: '/i/drive/file/:file', component: MkDrive },
 		{ path: '/selectdrive', component: MkSelectDrive },
 		{ path: '/:user', component: MkUser }
 	]);
diff --git a/src/web/app/mobile/views/components/drive.file-detail.vue b/src/web/app/mobile/views/components/drive.file-detail.vue
index db0c3c701..9a47eeb12 100644
--- a/src/web/app/mobile/views/components/drive.file-detail.vue
+++ b/src/web/app/mobile/views/components/drive.file-detail.vue
@@ -66,8 +66,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import EXIF from 'exif-js';
-import hljs from 'highlight.js';
+import * as EXIF from 'exif-js';
+import * as hljs from 'highlight.js';
 import gcd from '../../../common/scripts/gcd';
 
 export default Vue.extend({
diff --git a/src/web/app/mobile/views/components/drive.vue b/src/web/app/mobile/views/components/drive.vue
index 35d91d183..696c63e2a 100644
--- a/src/web/app/mobile/views/components/drive.vue
+++ b/src/web/app/mobile/views/components/drive.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-drive">
 	<nav ref="nav">
-		<a @click.prevent="goRoot" href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</a>
+		<a @click.prevent="goRoot()" href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</a>
 		<template v-for="folder in hierarchyFolders">
 			<span :key="folder.id + '>'">%fa:angle-right%</span>
 			<a :key="folder.id" @click.prevent="cd(folder)" :href="`/i/drive/folder/${folder.id}`">{{ folder.name }}</a>
@@ -158,7 +158,7 @@ export default Vue.extend({
 			this.file = null;
 
 			if (target == null) {
-				this.goRoot();
+				this.goRoot(silent);
 				return;
 			} else if (typeof target == 'object') {
 				target = target.id;
@@ -235,12 +235,12 @@ export default Vue.extend({
 			this.addFolder(folder, true);
 		},
 
-		goRoot() {
+		goRoot(silent = false) {
 			if (this.folder || this.file) {
 				this.file = null;
 				this.folder = null;
 				this.hierarchyFolders = [];
-				this.$emit('move-root');
+				this.$emit('move-root', silent);
 				this.fetch();
 			}
 		},
diff --git a/src/web/app/mobile/views/pages/drive.vue b/src/web/app/mobile/views/pages/drive.vue
index 1f442c224..689be04d8 100644
--- a/src/web/app/mobile/views/pages/drive.vue
+++ b/src/web/app/mobile/views/pages/drive.vue
@@ -2,15 +2,15 @@
 <mk-ui :func="fn">
 	<span slot="header">
 		<template v-if="folder">%fa:R folder-open%{{ folder.name }}</template>
-		<template v-if="file"><mk-file-type-icon class="icon"/>{{ file.name }}</template>
-		<template v-else>%fa:cloud%%i18n:mobile.tags.mk-drive-page.drive%</template>
+		<template v-if="file"><mk-file-type-icon class="icon" :type="file.type"/>{{ file.name }}</template>
+		<template v-if="!folder && !file">%fa:cloud%%i18n:mobile.tags.mk-drive-page.drive%</template>
 	</span>
 	<template slot="funcIcon">%fa:ellipsis-h%</template>
 	<mk-drive
 		ref="browser"
-		:init-folder="folder"
-		:init-file="file"
-		is-naked
+		:init-folder="initFolder"
+		:init-file="initFile"
+		:is-naked="true"
 		:top="48"
 		@begin-fetch="Progress.start()"
 		@fetched-mid="Progress.set(0.5)"
@@ -31,21 +31,43 @@ export default Vue.extend({
 		return {
 			Progress,
 			folder: null,
-			file: null
+			file: null,
+			initFolder: null,
+			initFile: null
 		};
 	},
+	created() {
+		this.initFolder = this.$route.params.folder;
+		this.initFile = this.$route.params.file;
+
+		window.addEventListener('popstate', this.onPopState);
+	},
 	mounted() {
 		document.title = 'Misskey Drive';
 	},
+	beforeDestroy() {
+		window.removeEventListener('popstate', this.onPopState);
+	},
 	methods: {
+		onPopState() {
+			if (this.$route.params.folder) {
+				(this.$refs as any).browser.cd(this.$route.params.folder, true);
+			} else if (this.$route.params.file) {
+				(this.$refs as any).browser.cf(this.$route.params.file, true);
+			} else {
+				(this.$refs as any).browser.goRoot(true);
+			}
+		},
 		fn() {
 			(this.$refs as any).browser.openContextMenu();
 		},
-		onMoveRoot() {
+		onMoveRoot(silent) {
 			const title = 'Misskey Drive';
 
-			// Rewrite URL
-			history.pushState(null, title, '/i/drive');
+			if (!silent) {
+				// Rewrite URL
+				history.pushState(null, title, '/i/drive');
+			}
 
 			document.title = title;
 

From 62d16db008df5847f9c77eabea8af4604ae573f0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 20:52:39 +0900
Subject: [PATCH 271/286] wip

---
 .../views/components/activity.calendar.vue        |  2 +-
 .../desktop/views/components/activity.chart.vue   | 10 ++++++----
 .../desktop/views/components/friends-maker.vue    | 10 +++++-----
 src/web/app/desktop/views/components/timeline.vue | 15 +++++++++++----
 .../desktop/views/components/widgets/users.vue    |  4 ++--
 5 files changed, 25 insertions(+), 16 deletions(-)

diff --git a/src/web/app/desktop/views/components/activity.calendar.vue b/src/web/app/desktop/views/components/activity.calendar.vue
index d9b852315..72233e9ac 100644
--- a/src/web/app/desktop/views/components/activity.calendar.vue
+++ b/src/web/app/desktop/views/components/activity.calendar.vue
@@ -37,7 +37,7 @@ export default Vue.extend({
 			d.x = x;
 			d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay();
 
-			d.v = d.total / (peak / 2);
+			d.v = peak == 0 ? 0 : d.total / (peak / 2);
 			if (d.v > 1) d.v = 1;
 			const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
 			const cs = d.v * 100;
diff --git a/src/web/app/desktop/views/components/activity.chart.vue b/src/web/app/desktop/views/components/activity.chart.vue
index e64b181ba..5057786ed 100644
--- a/src/web/app/desktop/views/components/activity.chart.vue
+++ b/src/web/app/desktop/views/components/activity.chart.vue
@@ -62,10 +62,12 @@ export default Vue.extend({
 	methods: {
 		render() {
 			const peak = Math.max.apply(null, this.data.map(d => d.total));
-			this.pointsPost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' ');
-			this.pointsReply = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
-			this.pointsRepost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' ');
-			this.pointsTotal = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
+			if (peak != 0) {
+				this.pointsPost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' ');
+				this.pointsReply = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
+				this.pointsRepost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' ');
+				this.pointsTotal = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
+			}
 		},
 		onMousedown(e) {
 			const clickX = e.clientX;
diff --git a/src/web/app/desktop/views/components/friends-maker.vue b/src/web/app/desktop/views/components/friends-maker.vue
index 61015b979..ab35efc75 100644
--- a/src/web/app/desktop/views/components/friends-maker.vue
+++ b/src/web/app/desktop/views/components/friends-maker.vue
@@ -3,20 +3,20 @@
 	<p class="title">気になるユーザーをフォロー:</p>
 	<div class="users" v-if="!fetching && users.length > 0">
 		<div class="user" v-for="user in users" :key="user.id">
-			<a class="avatar-anchor" :href="`/${user.username}`">
+			<router-link class="avatar-anchor" :to="`/${user.username}`">
 				<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="user.id"/>
-			</a>
+			</router-link>
 			<div class="body">
-				<a class="name" :href="`/${user.username}`" target="_blank" v-user-preview="user.id">{{ user.name }}</a>
+				<router-link class="name" :to="`/${user.username}`" v-user-preview="user.id">{{ user.name }}</router-link>
 				<p class="username">@{{ user.username }}</p>
 			</div>
-			<mk-follow-button user="user"/>
+			<mk-follow-button :user="user"/>
 		</div>
 	</div>
 	<p class="empty" v-if="!fetching && users.length == 0">おすすめのユーザーは見つかりませんでした。</p>
 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%読み込んでいます<mk-ellipsis/></p>
 	<a class="refresh" @click="refresh">もっと見る</a>
-	<button class="close" @click="$destroy" title="閉じる">%fa:times%</button>
+	<button class="close" @click="$destroy()" title="閉じる">%fa:times%</button>
 </div>
 </template>
 
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index a3f27412d..eef62104e 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -4,8 +4,15 @@
 	<div class="fetching" v-if="fetching">
 		<mk-ellipsis-icon/>
 	</div>
-	<p class="empty" v-if="posts.length == 0 && !fetching">%fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。</p>
-	<mk-posts :posts="posts" ref="timeline"/>
+	<p class="empty" v-if="posts.length == 0 && !fetching">
+		%fa:R comments%自分の投稿や、自分がフォローしているユーザーの投稿が表示されます。
+	</p>
+	<mk-posts :posts="posts" ref="timeline">
+		<div slot="footer">
+			<template v-if="!moreFetching">%fa:comments%</template>
+			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
+		</div>
+	</mk-posts>
 </div>
 </template>
 
@@ -69,8 +76,8 @@ export default Vue.extend({
 			(this as any).api('posts/timeline', {
 				until_id: this.posts[this.posts.length - 1].id
 			}).then(posts => {
+				this.posts = this.posts.concat(posts);
 				this.moreFetching = false;
-				this.posts.unshift(posts);
 			});
 		},
 		onPost(post) {
@@ -104,7 +111,7 @@ export default Vue.extend({
 	border solid 1px rgba(0, 0, 0, 0.075)
 	border-radius 6px
 
-	> .mk-following-setuper
+	> .mk-friends-maker
 		border-bottom solid 1px #eee
 
 	> .fetching
diff --git a/src/web/app/desktop/views/components/widgets/users.vue b/src/web/app/desktop/views/components/widgets/users.vue
index 4a9ab2aa3..f3a1509cf 100644
--- a/src/web/app/desktop/views/components/widgets/users.vue
+++ b/src/web/app/desktop/views/components/widgets/users.vue
@@ -7,11 +7,11 @@
 	<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" :href="`/${_user.username}`">
+			<router-link class="avatar-anchor" :to="`/${_user.username}`">
 				<img class="avatar" :src="`${_user.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
 			</router-link>
 			<div class="body">
-				<a class="name" :href="`/${_user.username}`" v-user-preview="_user.id">{{ _user.name }}</a>
+				<router-link class="name" :to="`/${_user.username}`" v-user-preview="_user.id">{{ _user.name }}</router-link>
 				<p class="username">@{{ _user.username }}</p>
 			</div>
 			<mk-follow-button :user="_user"/>

From 4fd3192791c9454fb900c3eea5da8aa429c980a8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 21:15:24 +0900
Subject: [PATCH 272/286] wip

---
 .../views/components/messaging-room.form.vue  |  6 ++--
 src/web/app/desktop/api/choose-drive-file.ts  | 34 +++++++++++++------
 src/web/app/desktop/script.ts                 |  2 ++
 .../components/messaging-room-window.vue      |  2 +-
 .../desktop/views/components/post-form.vue    |  3 --
 .../{api-setting.vue => settings.api.vue}     | 10 +++---
 .../app/desktop/views/components/settings.vue |  6 ++--
 .../desktop/views/directives/autocomplete.ts  |  6 ++--
 .../desktop/views/pages/messaging-room.vue    | 33 +++++++++++-------
 .../app/desktop/views/pages/selectdrive.vue   | 10 +++---
 src/web/app/desktop/views/pages/user/user.vue |  6 ++--
 11 files changed, 71 insertions(+), 47 deletions(-)
 rename src/web/app/desktop/views/components/{api-setting.vue => settings.api.vue} (80%)

diff --git a/src/web/app/common/views/components/messaging-room.form.vue b/src/web/app/common/views/components/messaging-room.form.vue
index 470606b77..b89365a5d 100644
--- a/src/web/app/common/views/components/messaging-room.form.vue
+++ b/src/web/app/common/views/components/messaging-room.form.vue
@@ -1,15 +1,15 @@
 <template>
 <div class="mk-messaging-form">
 	<textarea v-model="text" @keypress="onKeypress" @paste="onPaste" placeholder="%i18n:common.input-message-here%"></textarea>
-	<div class="files"></div>
+	<div class="file" v-if="file">{{ file.name }}</div>
 	<mk-uploader ref="uploader"/>
 	<button class="send" @click="send" :disabled="sending" title="%i18n:common.send%">
 		<template v-if="!sending">%fa:paper-plane%</template><template v-if="sending">%fa:spinner .spin%</template>
 	</button>
-	<button class="attach-from-local" type="button" title="%i18n:common.tags.mk-messaging-form.attach-from-local%">
+	<button class="attach-from-local" title="%i18n:common.tags.mk-messaging-form.attach-from-local%">
 		%fa:upload%
 	</button>
-	<button class="attach-from-drive" type="button" title="%i18n:common.tags.mk-messaging-form.attach-from-drive%">
+	<button class="attach-from-drive" @click="chooseFileFromDrive" title="%i18n:common.tags.mk-messaging-form.attach-from-drive%">
 		%fa:R folder-open%
 	</button>
 	<input name="file" type="file" accept="image/*"/>
diff --git a/src/web/app/desktop/api/choose-drive-file.ts b/src/web/app/desktop/api/choose-drive-file.ts
index e04844171..892036244 100644
--- a/src/web/app/desktop/api/choose-drive-file.ts
+++ b/src/web/app/desktop/api/choose-drive-file.ts
@@ -1,18 +1,30 @@
+import { url } from '../../config';
 import MkChooseFileFromDriveWindow from '../views/components/choose-file-from-drive-window.vue';
 
 export default function(opts) {
 	return new Promise((res, rej) => {
 		const o = opts || {};
-		const w = new MkChooseFileFromDriveWindow({
-			propsData: {
-				title: o.title,
-				multiple: o.multiple,
-				initFolder: o.currentFolder
-			}
-		}).$mount();
-		w.$once('selected', file => {
-			res(file);
-		});
-		document.body.appendChild(w.$el);
+
+		if (document.body.clientWidth > 800) {
+			const w = new MkChooseFileFromDriveWindow({
+				propsData: {
+					title: o.title,
+					multiple: o.multiple,
+					initFolder: o.currentFolder
+				}
+			}).$mount();
+			w.$once('selected', file => {
+				res(file);
+			});
+			document.body.appendChild(w.$el);
+		} else {
+			window['cb'] = file => {
+				res(file);
+			};
+
+			window.open(url + '/selectdrive',
+				'drive_window',
+				'height=500, width=800');
+		}
 	});
 }
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index 3c560033f..e584de3dd 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -24,6 +24,7 @@ import MkUser from './views/pages/user/user.vue';
 import MkSelectDrive from './views/pages/selectdrive.vue';
 import MkDrive from './views/pages/drive.vue';
 import MkHomeCustomize from './views/pages/home-customize.vue';
+import MkMessagingRoom from './views/pages/messaging-room.vue';
 
 /**
  * init
@@ -70,6 +71,7 @@ init(async (launch) => {
 	app.$router.addRoutes([
 		{ path: '/', name: 'index', component: MkIndex },
 		{ path: '/i/customize-home', component: MkHomeCustomize },
+		{ path: '/i/messaging/:username', component: MkMessagingRoom },
 		{ path: '/i/drive', component: MkDrive },
 		{ path: '/i/drive/folder/:folder', component: MkDrive },
 		{ path: '/selectdrive', component: MkSelectDrive },
diff --git a/src/web/app/desktop/views/components/messaging-room-window.vue b/src/web/app/desktop/views/components/messaging-room-window.vue
index f93990d89..66a9aa003 100644
--- a/src/web/app/desktop/views/components/messaging-room-window.vue
+++ b/src/web/app/desktop/views/components/messaging-room-window.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-window ref="window" width="500px" height="560px" :popout="popout" @closed="$destroy">
+<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
 	<span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ user.name }}</span>
 	<mk-messaging-room :user="user" :class="$style.content"/>
 </mk-window>
diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index f63584806..1c152910e 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -111,9 +111,6 @@ export default Vue.extend({
 			}
 		});
 	},
-	beforeDestroy() {
-		this.autocomplete.detach();
-	},
 	methods: {
 		focus() {
 			(this.$refs.text as any).focus();
diff --git a/src/web/app/desktop/views/components/api-setting.vue b/src/web/app/desktop/views/components/settings.api.vue
similarity index 80%
rename from src/web/app/desktop/views/components/api-setting.vue
rename to src/web/app/desktop/views/components/settings.api.vue
index 08c5a0c51..5831f8207 100644
--- a/src/web/app/desktop/views/components/api-setting.vue
+++ b/src/web/app/desktop/views/components/settings.api.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-api-setting">
+<div class="root api">
 	<p>Token: <code>{{ os.i.token }}</code></p>
 	<p>%i18n:desktop.tags.mk-api-info.intro%</p>
 	<div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-api-info.caution%</p></div>
@@ -10,12 +10,14 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import passwordDialog from '../../scripts/password-dialog';
 
 export default Vue.extend({
 	methods: {
 		regenerateToken() {
-			passwordDialog('%i18n:desktop.tags.mk-api-info.enter-password%', password => {
+			(this as any).apis.input({
+				title: '%i18n:desktop.tags.mk-api-info.enter-password%',
+				type: 'password'
+			}).then(password => {
 				(this as any).api('i/regenerate_token', {
 					password: password
 				});
@@ -26,7 +28,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-api-setting
+.root.api
 	color #4a535a
 
 	code
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index b36698b64..767ec3f96 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -60,7 +60,7 @@
 
 		<section class="api" v-show="page == 'api'">
 			<h1>API</h1>
-			<mk-api-info/>
+			<x-api/>
 		</section>
 
 		<section class="other" v-show="page == 'other'">
@@ -77,13 +77,15 @@ import XProfile from './settings.profile.vue';
 import XMute from './settings.mute.vue';
 import XPassword from './settings.password.vue';
 import X2fa from './settings.2fa.vue';
+import XApi from './settings.api.vue';
 
 export default Vue.extend({
 	components: {
 		XProfile,
 		XMute,
 		XPassword,
-		X2fa
+		X2fa,
+		XApi
 	},
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/directives/autocomplete.ts b/src/web/app/desktop/views/directives/autocomplete.ts
index 35a3b2e9c..53fa5a4df 100644
--- a/src/web/app/desktop/views/directives/autocomplete.ts
+++ b/src/web/app/desktop/views/directives/autocomplete.ts
@@ -3,14 +3,14 @@ import MkAutocomplete from '../components/autocomplete.vue';
 
 export default {
 	bind(el, binding, vn) {
-		const self = el._userPreviewDirective_ = {} as any;
+		const self = el._autoCompleteDirective_ = {} as any;
 		self.x = new Autocomplete(el);
 		self.x.attach();
 	},
 
 	unbind(el, binding, vn) {
-		const self = el._userPreviewDirective_;
-		self.x.close();
+		const self = el._autoCompleteDirective_;
+		self.x.detach();
 	}
 };
 
diff --git a/src/web/app/desktop/views/pages/messaging-room.vue b/src/web/app/desktop/views/pages/messaging-room.vue
index ace9e1607..d71a93b24 100644
--- a/src/web/app/desktop/views/pages/messaging-room.vue
+++ b/src/web/app/desktop/views/pages/messaging-room.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-messaging-room-page">
-	<mk-messaging-room v-if="user" :user="user" is-naked/>
+	<mk-messaging-room v-if="user" :user="user" :is-naked="true"/>
 </div>
 </template>
 
@@ -9,28 +9,37 @@ import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
-	props: ['username'],
 	data() {
 		return {
 			fetching: true,
 			user: null
 		};
 	},
+	watch: {
+		$route: 'fetch'
+	},
+	created() {
+		this.fetch();
+	},
 	mounted() {
-		Progress.start();
-
 		document.documentElement.style.background = '#fff';
+	},
+	methods: {
+		fetch() {
+			Progress.start();
+			this.fetching = true;
 
-		(this as any).api('users/show', {
-			username: this.username
-		}).then(user => {
-			this.user = user;
-			this.fetching = false;
+			(this as any).api('users/show', {
+				username: this.$route.params.username
+			}).then(user => {
+				this.user = user;
+				this.fetching = false;
 
-			document.title = 'メッセージ: ' + this.user.name;
+				document.title = 'メッセージ: ' + this.user.name;
 
-			Progress.done();
-		});
+				Progress.done();
+			});
+		}
 	}
 });
 </script>
diff --git a/src/web/app/desktop/views/pages/selectdrive.vue b/src/web/app/desktop/views/pages/selectdrive.vue
index da31ef8f0..b1f00da2b 100644
--- a/src/web/app/desktop/views/pages/selectdrive.vue
+++ b/src/web/app/desktop/views/pages/selectdrive.vue
@@ -1,15 +1,15 @@
 <template>
-<div class="mk-selectdrive">
+<div class="mkp-selectdrive">
 	<mk-drive ref="browser"
 		:multiple="multiple"
 		@selected="onSelected"
 		@change-selection="onChangeSelection"
 	/>
-	<div>
+	<footer>
 		<button class="upload" title="%i18n:desktop.tags.mk-selectdrive-page.upload%" @click="upload">%fa:upload%</button>
 		<button class="cancel" @click="close">%i18n:desktop.tags.mk-selectdrive-page.cancel%</button>
 		<button class="ok" @click="ok">%i18n:desktop.tags.mk-selectdrive-page.ok%</button>
-	</div>
+	</footer>
 </div>
 </template>
 
@@ -54,7 +54,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-selectdrive
+.mkp-selectdrive
 	display block
 	position fixed
 	width 100%
@@ -64,7 +64,7 @@ export default Vue.extend({
 	> .mk-drive
 		height calc(100% - 72px)
 
-	> div
+	> footer
 		position fixed
 		bottom 0
 		left 0
diff --git a/src/web/app/desktop/views/pages/user/user.vue b/src/web/app/desktop/views/pages/user/user.vue
index 095df0e48..1ce3fa27e 100644
--- a/src/web/app/desktop/views/pages/user/user.vue
+++ b/src/web/app/desktop/views/pages/user/user.vue
@@ -29,12 +29,12 @@ export default Vue.extend({
 			user: null
 		};
 	},
-	created() {
-		this.fetch();
-	},
 	watch: {
 		$route: 'fetch'
 	},
+	created() {
+		this.fetch();
+	},
 	methods: {
 		fetch() {
 			this.fetching = true;

From 1025077df21d63ba50fea7ec0058db5e698b4068 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 21:23:10 +0900
Subject: [PATCH 273/286] wip

---
 src/web/app/desktop/script.ts                 |  4 ++-
 .../desktop/views/components/post-detail.vue  | 26 ++++++++--------
 src/web/app/desktop/views/pages/post.vue      | 30 +++++++++++--------
 src/web/app/mobile/script.ts                  |  4 ++-
 src/web/app/mobile/views/pages/post.vue       | 29 +++++++++++-------
 src/web/app/mobile/views/pages/user.vue       |  6 ++--
 6 files changed, 60 insertions(+), 39 deletions(-)

diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index e584de3dd..6c40ae0a3 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -25,6 +25,7 @@ import MkSelectDrive from './views/pages/selectdrive.vue';
 import MkDrive from './views/pages/drive.vue';
 import MkHomeCustomize from './views/pages/home-customize.vue';
 import MkMessagingRoom from './views/pages/messaging-room.vue';
+import MkPost from './views/pages/post.vue';
 
 /**
  * init
@@ -75,7 +76,8 @@ init(async (launch) => {
 		{ path: '/i/drive', component: MkDrive },
 		{ path: '/i/drive/folder/:folder', component: MkDrive },
 		{ path: '/selectdrive', component: MkSelectDrive },
-		{ path: '/:user', component: MkUser }
+		{ path: '/:user', component: MkUser },
+		{ path: '/:user/:post', component: MkPost }
 	]);
 }, true);
 
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index 6eca03520..cac4671c5 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -38,7 +38,7 @@
 			</router-link>
 		</header>
 		<div class="body">
-			<mk-post-html v-if="p.ast" :ast="p.ast" :i="os.i"/>
+			<mk-post-html :class="$style.text" v-if="p.ast" :ast="p.ast" :i="os.i"/>
 			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 			<div class="media" v-if="p.media">
 				<mk-images :images="p.media"/>
@@ -311,17 +311,8 @@ export default Vue.extend({
 		> .body
 			padding 8px 0
 
-			> .text
-				cursor default
-				display block
-				margin 0
-				padding 0
-				overflow-wrap break-word
-				font-size 1.5em
-				color #717171
-
-				> .mk-url-preview
-					margin-top 8px
+			> .mk-url-preview
+				margin-top 8px
 
 		> footer
 			font-size 1.2em
@@ -351,3 +342,14 @@ export default Vue.extend({
 			border-top 1px solid #eef0f2
 
 </style>
+
+<style lang="stylus" module>
+.text
+	cursor default
+	display block
+	margin 0
+	padding 0
+	overflow-wrap break-word
+	font-size 1.5em
+	color #717171
+</style>
diff --git a/src/web/app/desktop/views/pages/post.vue b/src/web/app/desktop/views/pages/post.vue
index 446fdbcbf..c7b8729b7 100644
--- a/src/web/app/desktop/views/pages/post.vue
+++ b/src/web/app/desktop/views/pages/post.vue
@@ -13,26 +13,32 @@ import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
-	props: ['postId'],
 	data() {
 		return {
 			fetching: true,
 			post: null
 		};
 	},
-	mounted() {
-		Progress.start();
+	watch: {
+		$route: 'fetch'
+	},
+	created() {
+		this.fetch();
+	},
+	methods: {
+		fetch() {
+			Progress.start();
+			this.fetching = true;
 
-		// TODO: extract the fetch step for vue-router's caching
+			(this as any).api('posts/show', {
+				post_id: this.$route.params.post
+			}).then(post => {
+				this.post = post;
+				this.fetching = false;
 
-		(this as any).api('posts/show', {
-			post_id: this.postId
-		}).then(post => {
-			this.post = post;
-			this.fetching = false;
-
-			Progress.done();
-		});
+				Progress.done();
+			});
+		}
 	}
 });
 </script>
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index 07912a178..dce6640ea 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -22,6 +22,7 @@ import MkDrive from './views/pages/drive.vue';
 import MkNotifications from './views/pages/notifications.vue';
 import MkMessaging from './views/pages/messaging.vue';
 import MkMessagingRoom from './views/pages/messaging-room.vue';
+import MkPost from './views/pages/post.vue';
 
 /**
  * init
@@ -57,6 +58,7 @@ init((launch) => {
 		{ path: '/i/drive/folder/:folder', component: MkDrive },
 		{ path: '/i/drive/file/:file', component: MkDrive },
 		{ path: '/selectdrive', component: MkSelectDrive },
-		{ path: '/:user', component: MkUser }
+		{ path: '/:user', component: MkUser },
+		{ path: '/:user/:post', component: MkPost }
 	]);
 }, true);
diff --git a/src/web/app/mobile/views/pages/post.vue b/src/web/app/mobile/views/pages/post.vue
index 03e9972a4..c62a001f2 100644
--- a/src/web/app/mobile/views/pages/post.vue
+++ b/src/web/app/mobile/views/pages/post.vue
@@ -16,27 +16,36 @@ import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
-	props: ['postId'],
 	data() {
 		return {
 			fetching: true,
 			post: null
 		};
 	},
+	watch: {
+		$route: 'fetch'
+	},
+	created() {
+		this.fetch();
+	},
 	mounted() {
 		document.title = 'Misskey';
 		document.documentElement.style.background = '#313a42';
+	},
+	methods: {
+		fetch() {
+			Progress.start();
+			this.fetching = true;
 
-		Progress.start();
+			(this as any).api('posts/show', {
+				post_id: this.$route.params.post
+			}).then(post => {
+				this.post = post;
+				this.fetching = false;
 
-		(this as any).api('posts/show', {
-			post_id: this.postId
-		}).then(post => {
-			this.post = post;
-			this.fetching = false;
-
-			Progress.done();
-		});
+				Progress.done();
+			});
+		}
 	}
 });
 </script>
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index b76f0ac84..335b2bc1e 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -82,12 +82,12 @@ export default Vue.extend({
 			return age(this.user.profile.birthday);
 		}
 	},
-	created() {
-		this.fetch();
-	},
 	watch: {
 		$route: 'fetch'
 	},
+	created() {
+		this.fetch();
+	},
 	mounted() {
 		document.documentElement.style.background = '#313a42';
 	},

From 84c157913ac2b7837085e4ced7ae28977c07e262 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 21:29:04 +0900
Subject: [PATCH 274/286] wip

---
 src/web/app/desktop/views/components/home.vue | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 1191ad895..6b2d75d84 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -274,6 +274,10 @@ export default Vue.extend({
 		> *
 			.customize-container
 				cursor move
+				border-radius 6px
+
+				&:hover
+					box-shadow 0 0 8px rgba(64, 120, 200, 0.3)
 
 				> *
 					pointer-events none

From 781628231823864b5e28cb1c018ae5a04dc9b83e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 21:39:36 +0900
Subject: [PATCH 275/286] wip

---
 src/web/app/desktop/router.ts                 | 100 ------------
 src/web/app/desktop/script.ts                 |   2 +
 .../desktop/views/pages/user/user.home.vue    |  24 +--
 .../views/pages/user/user.timeline.vue        |   4 +
 src/web/app/mobile/router.ts                  | 143 ------------------
 src/web/app/mobile/script.ts                  |   6 +
 src/web/app/mobile/tags/page/entrance.tag     |  66 --------
 .../app/mobile/tags/page/entrance/signin.tag  |  52 -------
 .../app/mobile/tags/page/entrance/signup.tag  |  38 -----
 .../tags/page/settings/authorized-apps.tag    |  17 ---
 .../app/mobile/tags/page/settings/signin.tag  |  17 ---
 .../app/mobile/tags/page/settings/twitter.tag |  17 ---
 12 files changed, 24 insertions(+), 462 deletions(-)
 delete mode 100644 src/web/app/desktop/router.ts
 delete mode 100644 src/web/app/mobile/router.ts
 delete mode 100644 src/web/app/mobile/tags/page/entrance.tag
 delete mode 100644 src/web/app/mobile/tags/page/entrance/signin.tag
 delete mode 100644 src/web/app/mobile/tags/page/entrance/signup.tag
 delete mode 100644 src/web/app/mobile/tags/page/settings/authorized-apps.tag
 delete mode 100644 src/web/app/mobile/tags/page/settings/signin.tag
 delete mode 100644 src/web/app/mobile/tags/page/settings/twitter.tag

diff --git a/src/web/app/desktop/router.ts b/src/web/app/desktop/router.ts
deleted file mode 100644
index 6ba8bda12..000000000
--- a/src/web/app/desktop/router.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-/**
- * Desktop App Router
- */
-
-import * as riot from 'riot';
-import * as route from 'page';
-import MiOS from '../common/mios';
-let page = null;
-
-export default (mios: MiOS) => {
-	route('/',                       index);
-	route('/selectdrive',            selectDrive);
-	route('/i/customize-home',       customizeHome);
-	route('/i/drive',                drive);
-	route('/i/drive/folder/:folder', drive);
-	route('/i/messaging/:user',      messaging);
-	route('/i/mentions',             mentions);
-	route('/post::post',             post);
-	route('/search',                 search);
-	route('/:user',                  user.bind(null, 'home'));
-	route('/:user/graphs',           user.bind(null, 'graphs'));
-	route('/:user/:post',            post);
-	route('*',                       notFound);
-
-	function index() {
-		mios.isSignedIn ? home() : entrance();
-	}
-
-	function home() {
-		mount(document.createElement('mk-home-page'));
-	}
-
-	function customizeHome() {
-		mount(document.createElement('mk-home-customize-page'));
-	}
-
-	function entrance() {
-		mount(document.createElement('mk-entrance'));
-		document.documentElement.setAttribute('data-page', 'entrance');
-	}
-
-	function mentions() {
-		const el = document.createElement('mk-home-page');
-		el.setAttribute('mode', 'mentions');
-		mount(el);
-	}
-
-	function search(ctx) {
-		const el = document.createElement('mk-search-page');
-		el.setAttribute('query', ctx.querystring.substr(2));
-		mount(el);
-	}
-
-	function user(page, ctx) {
-		const el = document.createElement('mk-user-page');
-		el.setAttribute('user', ctx.params.user);
-		el.setAttribute('page', page);
-		mount(el);
-	}
-
-	function post(ctx) {
-		const el = document.createElement('mk-post-page');
-		el.setAttribute('post', ctx.params.post);
-		mount(el);
-	}
-
-	function selectDrive() {
-		mount(document.createElement('mk-selectdrive-page'));
-	}
-
-	function drive(ctx) {
-		const el = document.createElement('mk-drive-page');
-		if (ctx.params.folder) el.setAttribute('folder', ctx.params.folder);
-		mount(el);
-	}
-
-	function messaging(ctx) {
-		const el = document.createElement('mk-messaging-room-page');
-		el.setAttribute('user', ctx.params.user);
-		mount(el);
-	}
-
-	function notFound() {
-		mount(document.createElement('mk-not-found'));
-	}
-
-	(riot as any).mixin('page', {
-		page: route
-	});
-
-	// EXEC
-	(route as any)();
-};
-
-function mount(content) {
-	document.documentElement.removeAttribute('data-page');
-	if (page) page.unmount();
-	const body = document.getElementById('app');
-	page = riot.mount(body.appendChild(content))[0];
-}
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index 6c40ae0a3..e7c8f8e49 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -26,6 +26,7 @@ import MkDrive from './views/pages/drive.vue';
 import MkHomeCustomize from './views/pages/home-customize.vue';
 import MkMessagingRoom from './views/pages/messaging-room.vue';
 import MkPost from './views/pages/post.vue';
+import MkSearch from './views/pages/search.vue';
 
 /**
  * init
@@ -76,6 +77,7 @@ init(async (launch) => {
 		{ path: '/i/drive', component: MkDrive },
 		{ path: '/i/drive/folder/:folder', component: MkDrive },
 		{ path: '/selectdrive', component: MkSelectDrive },
+		{ path: '/search', component: MkSearch },
 		{ path: '/:user', component: MkUser },
 		{ path: '/:user/:post', component: MkPost }
 	]);
diff --git a/src/web/app/desktop/views/pages/user/user.home.vue b/src/web/app/desktop/views/pages/user/user.home.vue
index bf96741cb..dbf02bd07 100644
--- a/src/web/app/desktop/views/pages/user/user.home.vue
+++ b/src/web/app/desktop/views/pages/user/user.home.vue
@@ -10,7 +10,7 @@
 	</div>
 	<main>
 		<mk-post-detail v-if="user.pinned_post" :post="user.pinned_post" compact/>
-		<x-timeline ref="tl" :user="user"/>
+		<x-timeline class="timeline" ref="tl" :user="user"/>
 	</main>
 	<div>
 		<div ref="right">
@@ -25,19 +25,19 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import XUserTimeline from './user.timeline.vue';
-import XUserProfile from './user.profile.vue';
-import XUserPhotos from './user.photos.vue';
-import XUserFollowersYouKnow from './user.followers-you-know.vue';
-import XUserFriends from './user.friends.vue';
+import XTimeline from './user.timeline.vue';
+import XProfile from './user.profile.vue';
+import XPhotos from './user.photos.vue';
+import XFollowersYouKnow from './user.followers-you-know.vue';
+import XFriends from './user.friends.vue';
 
 export default Vue.extend({
 	components: {
-		XUserTimeline,
-		XUserProfile,
-		XUserPhotos,
-		XUserFollowersYouKnow,
-		XUserFriends
+		XTimeline,
+		XProfile,
+		XPhotos,
+		XFollowersYouKnow,
+		XFriends
 	},
 	props: ['user'],
 	methods: {
@@ -64,7 +64,7 @@ export default Vue.extend({
 		padding 16px
 		width calc(100% - 275px * 2)
 
-		> .mk-user-timeline
+		> .timeline
 			border solid 1px rgba(0, 0, 0, 0.075)
 			border-radius 6px
 
diff --git a/src/web/app/desktop/views/pages/user/user.timeline.vue b/src/web/app/desktop/views/pages/user/user.timeline.vue
index 51c7589fd..d8fff6ce6 100644
--- a/src/web/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/web/app/desktop/views/pages/user/user.timeline.vue
@@ -87,6 +87,10 @@ export default Vue.extend({
 			if (current > document.body.offsetHeight - 16/*遊び*/) {
 				this.more();
 			}
+		},
+		warp(date) {
+			this.date = date;
+			this.fetch();
 		}
 	}
 });
diff --git a/src/web/app/mobile/router.ts b/src/web/app/mobile/router.ts
deleted file mode 100644
index 050fa7fc2..000000000
--- a/src/web/app/mobile/router.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-/**
- * Mobile App Router
- */
-
-import * as riot from 'riot';
-import * as route from 'page';
-import MiOS from '../common/mios';
-let page = null;
-
-export default (mios: MiOS) => {
-	route('/',                           index);
-	route('/selectdrive',                selectDrive);
-	route('/i/notifications',            notifications);
-	route('/i/messaging',                messaging);
-	route('/i/messaging/:username',      messaging);
-	route('/i/drive',                    drive);
-	route('/i/drive/folder/:folder',     drive);
-	route('/i/drive/file/:file',         drive);
-	route('/i/settings',                 settings);
-	route('/i/settings/profile',         settingsProfile);
-	route('/i/settings/signin-history',  settingsSignin);
-	route('/i/settings/twitter',         settingsTwitter);
-	route('/i/settings/authorized-apps', settingsAuthorizedApps);
-	route('/post/new',                   newPost);
-	route('/post::post',                 post);
-	route('/search',                     search);
-	route('/:user',                      user.bind(null, 'overview'));
-	route('/:user/graphs',               user.bind(null, 'graphs'));
-	route('/:user/followers',            userFollowers);
-	route('/:user/following',            userFollowing);
-	route('/:user/:post',                post);
-	route('*',                           notFound);
-
-	function index() {
-		mios.isSignedIn ? home() : entrance();
-	}
-
-	function home() {
-		mount(document.createElement('mk-home-page'));
-	}
-
-	function entrance() {
-		mount(document.createElement('mk-entrance'));
-	}
-
-	function notifications() {
-		mount(document.createElement('mk-notifications-page'));
-	}
-
-	function messaging(ctx) {
-		if (ctx.params.username) {
-			const el = document.createElement('mk-messaging-room-page');
-			el.setAttribute('username', ctx.params.username);
-			mount(el);
-		} else {
-			mount(document.createElement('mk-messaging-page'));
-		}
-	}
-
-	function newPost() {
-		mount(document.createElement('mk-new-post-page'));
-	}
-
-	function settings() {
-		mount(document.createElement('mk-settings-page'));
-	}
-
-	function settingsProfile() {
-		mount(document.createElement('mk-profile-setting-page'));
-	}
-
-	function settingsSignin() {
-		mount(document.createElement('mk-signin-history-page'));
-	}
-
-	function settingsTwitter() {
-		mount(document.createElement('mk-twitter-setting-page'));
-	}
-
-	function settingsAuthorizedApps() {
-		mount(document.createElement('mk-authorized-apps-page'));
-	}
-
-	function search(ctx) {
-		const el = document.createElement('mk-search-page');
-		el.setAttribute('query', ctx.querystring.substr(2));
-		mount(el);
-	}
-
-	function user(page, ctx) {
-		const el = document.createElement('mk-user-page');
-		el.setAttribute('user', ctx.params.user);
-		el.setAttribute('page', page);
-		mount(el);
-	}
-
-	function userFollowing(ctx) {
-		const el = document.createElement('mk-user-following-page');
-		el.setAttribute('user', ctx.params.user);
-		mount(el);
-	}
-
-	function userFollowers(ctx) {
-		const el = document.createElement('mk-user-followers-page');
-		el.setAttribute('user', ctx.params.user);
-		mount(el);
-	}
-
-	function post(ctx) {
-		const el = document.createElement('mk-post-page');
-		el.setAttribute('post', ctx.params.post);
-		mount(el);
-	}
-
-	function drive(ctx) {
-		const el = document.createElement('mk-drive-page');
-		if (ctx.params.folder) el.setAttribute('folder', ctx.params.folder);
-		if (ctx.params.file) el.setAttribute('file', ctx.params.file);
-		mount(el);
-	}
-
-	function selectDrive() {
-		mount(document.createElement('mk-selectdrive-page'));
-	}
-
-	function notFound() {
-		mount(document.createElement('mk-not-found'));
-	}
-
-	(riot as any).mixin('page', {
-		page: route
-	});
-
-	// EXEC
-	(route as any)();
-};
-
-function mount(content) {
-	document.documentElement.style.background = '#fff';
-	if (page) page.unmount();
-	const body = document.getElementById('app');
-	page = riot.mount(body.appendChild(content))[0];
-}
diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index dce6640ea..6e69b3ed3 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -23,6 +23,9 @@ import MkNotifications from './views/pages/notifications.vue';
 import MkMessaging from './views/pages/messaging.vue';
 import MkMessagingRoom from './views/pages/messaging-room.vue';
 import MkPost from './views/pages/post.vue';
+import MkSearch from './views/pages/search.vue';
+import MkFollowers from './views/pages/followers.vue';
+import MkFollowing from './views/pages/following.vue';
 
 /**
  * init
@@ -58,7 +61,10 @@ init((launch) => {
 		{ path: '/i/drive/folder/:folder', component: MkDrive },
 		{ path: '/i/drive/file/:file', component: MkDrive },
 		{ path: '/selectdrive', component: MkSelectDrive },
+		{ path: '/search', component: MkSearch },
 		{ path: '/:user', component: MkUser },
+		{ path: '/:user/followers', component: MkFollowers },
+		{ path: '/:user/following', component: MkFollowing },
 		{ path: '/:user/:post', component: MkPost }
 	]);
 }, true);
diff --git a/src/web/app/mobile/tags/page/entrance.tag b/src/web/app/mobile/tags/page/entrance.tag
deleted file mode 100644
index 17ba1cd7b..000000000
--- a/src/web/app/mobile/tags/page/entrance.tag
+++ /dev/null
@@ -1,66 +0,0 @@
-<mk-entrance>
-	<main><img src="/assets/title.svg" alt="Misskey"/>
-		<mk-entrance-signin v-if="mode == 'signin'"/>
-		<mk-entrance-signup v-if="mode == 'signup'"/>
-		<div class="introduction" v-if="mode == 'introduction'">
-			<mk-introduction/>
-			<button @click="signin">%i18n:common.ok%</button>
-		</div>
-	</main>
-	<footer>
-		<p class="c">{ _COPYRIGHT_ }</p>
-	</footer>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			height 100%
-
-			> main
-				display block
-
-				> img
-					display block
-					width 130px
-					height 120px
-					margin 0 auto
-
-				> .introduction
-					max-width 300px
-					margin 0 auto
-					color #666
-
-					> button
-						display block
-						margin 16px auto 0 auto
-
-			> footer
-				> .c
-					margin 0
-					text-align center
-					line-height 64px
-					font-size 10px
-					color rgba(#000, 0.5)
-
-	</style>
-	<script lang="typescript">
-		this.mode = 'signin';
-
-		this.signup = () => {
-			this.update({
-				mode: 'signup'
-			});
-		};
-
-		this.signin = () => {
-			this.update({
-				mode: 'signin'
-			});
-		};
-
-		this.introduction = () => {
-			this.update({
-				mode: 'introduction'
-			});
-		};
-	</script>
-</mk-entrance>
diff --git a/src/web/app/mobile/tags/page/entrance/signin.tag b/src/web/app/mobile/tags/page/entrance/signin.tag
deleted file mode 100644
index e6deea8c3..000000000
--- a/src/web/app/mobile/tags/page/entrance/signin.tag
+++ /dev/null
@@ -1,52 +0,0 @@
-<mk-entrance-signin>
-	<mk-signin/>
-	<a href={ _API_URL_ + '/signin/twitter' }>Twitterでサインイン</a>
-	<div class="divider"><span>or</span></div>
-	<button class="signup" @click="parent.signup">%i18n:mobile.tags.mk-entrance-signin.signup%</button><a class="introduction" @click="parent.introduction">%i18n:mobile.tags.mk-entrance-signin.about%</a>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0 auto
-			padding 0 8px
-			max-width 350px
-			text-align center
-
-			> .signup
-				padding 16px
-				width 100%
-				font-size 1em
-				color #fff
-				background $theme-color
-				border-radius 3px
-
-			> .divider
-				padding 16px 0
-				text-align center
-
-				&:after
-					content ""
-					display block
-					position absolute
-					top 50%
-					width 100%
-					height 1px
-					border-top solid 1px rgba(0, 0, 0, 0.1)
-
-				> *
-					z-index 1
-					padding 0 8px
-					color rgba(0, 0, 0, 0.5)
-					background #fdfdfd
-
-			> .introduction
-				display inline-block
-				margin-top 16px
-				font-size 12px
-				color #666
-
-
-
-
-
-	</style>
-</mk-entrance-signin>
diff --git a/src/web/app/mobile/tags/page/entrance/signup.tag b/src/web/app/mobile/tags/page/entrance/signup.tag
deleted file mode 100644
index d219bb100..000000000
--- a/src/web/app/mobile/tags/page/entrance/signup.tag
+++ /dev/null
@@ -1,38 +0,0 @@
-<mk-entrance-signup>
-	<mk-signup/>
-	<button class="cancel" type="button" @click="parent.signin" title="%i18n:mobile.tags.mk-entrance-signup.cancel%">%fa:times%</button>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0 auto
-			padding 0 8px
-			max-width 350px
-
-			> .cancel
-				cursor pointer
-				display block
-				position absolute
-				top 0
-				right 0
-				z-index 1
-				margin 0
-				padding 0
-				font-size 1.2em
-				color #999
-				border none
-				outline none
-				box-shadow none
-				background transparent
-				transition opacity 0.1s ease
-
-				&:hover
-					color #555
-
-				&:active
-					color #222
-
-				> [data-fa]
-					padding 14px
-
-	</style>
-</mk-entrance-signup>
diff --git a/src/web/app/mobile/tags/page/settings/authorized-apps.tag b/src/web/app/mobile/tags/page/settings/authorized-apps.tag
deleted file mode 100644
index 35cc961f0..000000000
--- a/src/web/app/mobile/tags/page/settings/authorized-apps.tag
+++ /dev/null
@@ -1,17 +0,0 @@
-<mk-authorized-apps-page>
-	<mk-ui ref="ui">
-		<mk-authorized-apps/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import ui from '../../../scripts/ui-event';
-
-		this.on('mount', () => {
-			document.title = 'Misskey | %i18n:mobile.tags.mk-authorized-apps-page.application%';
-			ui.trigger('title', '%fa:puzzle-piece%%i18n:mobile.tags.mk-authorized-apps-page.application%');
-		});
-	</script>
-</mk-authorized-apps-page>
diff --git a/src/web/app/mobile/tags/page/settings/signin.tag b/src/web/app/mobile/tags/page/settings/signin.tag
deleted file mode 100644
index 7a57406c1..000000000
--- a/src/web/app/mobile/tags/page/settings/signin.tag
+++ /dev/null
@@ -1,17 +0,0 @@
-<mk-signin-history-page>
-	<mk-ui ref="ui">
-		<mk-signin-history/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import ui from '../../../scripts/ui-event';
-
-		this.on('mount', () => {
-			document.title = 'Misskey | %i18n:mobile.tags.mk-signin-history-page.signin-history%';
-			ui.trigger('title', '%fa:sign-in-alt%%i18n:mobile.tags.mk-signin-history-page.signin-history%');
-		});
-	</script>
-</mk-signin-history-page>
diff --git a/src/web/app/mobile/tags/page/settings/twitter.tag b/src/web/app/mobile/tags/page/settings/twitter.tag
deleted file mode 100644
index ca5fe2c43..000000000
--- a/src/web/app/mobile/tags/page/settings/twitter.tag
+++ /dev/null
@@ -1,17 +0,0 @@
-<mk-twitter-setting-page>
-	<mk-ui ref="ui">
-		<mk-twitter-setting/>
-	</mk-ui>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-	</style>
-	<script lang="typescript">
-		import ui from '../../../scripts/ui-event';
-
-		this.on('mount', () => {
-			document.title = 'Misskey | %i18n:mobile.tags.mk-twitter-setting-page.twitter-integration%';
-			ui.trigger('title', '%fa:B twitter%%i18n:mobile.tags.mk-twitter-setting-page.twitter-integration%');
-		});
-	</script>
-</mk-twitter-setting-page>

From bc632cefe51d9d9cb50d787c8141365429e74396 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 22:03:44 +0900
Subject: [PATCH 276/286] wip

---
 src/web/app/common/views/components/index.ts  |   2 +
 src/web/app/common/views/components/poll.vue  | 164 +++++++++---------
 src/web/app/desktop/views/components/home.vue |   4 +-
 .../views/components/notifications.vue        |   2 +-
 .../app/desktop/views/components/posts.vue    |   5 +-
 src/web/app/mobile/views/components/index.ts  |   2 +
 .../mobile/views/components/notification.vue  |   1 +
 .../mobile/views/components/notifications.vue |   3 +-
 src/web/app/mobile/views/components/posts.vue |   7 +-
 src/web/app/mobile/views/pages/post.vue       |   2 +-
 src/web/app/mobile/views/pages/user.vue       |   2 +-
 11 files changed, 104 insertions(+), 90 deletions(-)

diff --git a/src/web/app/common/views/components/index.ts b/src/web/app/common/views/components/index.ts
index d3f6a425f..ab0f1767d 100644
--- a/src/web/app/common/views/components/index.ts
+++ b/src/web/app/common/views/components/index.ts
@@ -5,6 +5,7 @@ import signup from './signup.vue';
 import forkit from './forkit.vue';
 import nav from './nav.vue';
 import postHtml from './post-html';
+import poll from './poll.vue';
 import pollEditor from './poll-editor.vue';
 import reactionIcon from './reaction-icon.vue';
 import reactionsViewer from './reactions-viewer.vue';
@@ -25,6 +26,7 @@ Vue.component('mk-signup', signup);
 Vue.component('mk-forkit', forkit);
 Vue.component('mk-nav', nav);
 Vue.component('mk-post-html', postHtml);
+Vue.component('mk-poll', poll);
 Vue.component('mk-poll-editor', pollEditor);
 Vue.component('mk-reaction-icon', reactionIcon);
 Vue.component('mk-reactions-viewer', reactionsViewer);
diff --git a/src/web/app/common/views/components/poll.vue b/src/web/app/common/views/components/poll.vue
index d06c019db..7ed5bc6b1 100644
--- a/src/web/app/common/views/components/poll.vue
+++ b/src/web/app/common/views/components/poll.vue
@@ -1,11 +1,11 @@
 <template>
-<div :data-is-voted="isVoted">
+<div class="mk-poll" :data-is-voted="isVoted">
 	<ul>
-		<li v-for="choice in poll.choices" :key="choice.id" @click="vote.bind(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', choice.text) : ''">
-			<div class="backdrop" :style="{ 'width:' + (showResult ? (choice.votes / total * 100) : 0) + '%' }"></div>
+		<li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? '%i18n:common.tags.mk-poll.vote-to%'.replace('{}', choice.text) : ''">
+			<div class="backdrop" :style="{ 'width': (showResult ? (choice.votes / total * 100) : 0) + '%' }"></div>
 			<span>
 				<template v-if="choice.is_voted">%fa:check%</template>
-				{{ text }}
+				{{ choice.text }}
 				<span class="votes" v-if="showResult">({{ '%i18n:common.tags.mk-poll.vote-count%'.replace('{}', choice.votes) }})</span>
 			</span>
 		</li>
@@ -19,100 +19,100 @@
 </div>
 </template>
 
-<script lang="typescript">
-	export default {
-		props: ['post'],
-		data() {
-			return {
-				showResult: false
-			};
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['post'],
+	data() {
+		return {
+			showResult: false
+		};
+	},
+	computed: {
+		poll(): any {
+			return this.post.poll;
 		},
-		computed: {
-			poll() {
-				return this.post.poll;
-			},
-			total() {
-				return this.poll.choices.reduce((a, b) => a + b.votes, 0);
-			},
-			isVoted() {
-				return this.poll.choices.some(c => c.is_voted);
-			}
+		total(): number {
+			return this.poll.choices.reduce((a, b) => a + b.votes, 0);
 		},
-		created() {
-			this.showResult = this.isVoted;
-		},
-		methods: {
-			toggleShowResult() {
-				this.showResult = !this.showResult;
-			},
-			vote(id) {
-				if (this.poll.choices.some(c => c.is_voted)) return;
-				(this as any).api('posts/polls/vote', {
-					post_id: this.post.id,
-					choice: id
-				}).then(() => {
-					this.poll.choices.forEach(c => {
-						if (c.id == id) {
-							c.votes++;
-							c.is_voted = true;
-						}
-					});
-					this.showResult = true;
-				});
-			}
+		isVoted(): boolean {
+			return this.poll.choices.some(c => c.is_voted);
 		}
-	};
+	},
+	created() {
+		this.showResult = this.isVoted;
+	},
+	methods: {
+		toggleShowResult() {
+			this.showResult = !this.showResult;
+		},
+		vote(id) {
+			if (this.poll.choices.some(c => c.is_voted)) return;
+			(this as any).api('posts/polls/vote', {
+				post_id: this.post.id,
+				choice: id
+			}).then(() => {
+				this.poll.choices.forEach(c => {
+					if (c.id == id) {
+						c.votes++;
+						Vue.set(c, 'is_voted', true);
+					}
+				});
+				this.showResult = true;
+			});
+		}
+	}
+});
 </script>
 
 <style lang="stylus" scoped>
-	:scope
+.mk-poll
+
+	> ul
 		display block
+		margin 0
+		padding 0
+		list-style none
 
-		> ul
+		> li
 			display block
-			margin 0
-			padding 0
-			list-style none
+			margin 4px 0
+			padding 4px 8px
+			width 100%
+			border solid 1px #eee
+			border-radius 4px
+			overflow hidden
+			cursor pointer
 
-			> li
-				display block
-				margin 4px 0
-				padding 4px 8px
-				width 100%
-				border solid 1px #eee
-				border-radius 4px
-				overflow hidden
-				cursor pointer
+			&:hover
+				background rgba(0, 0, 0, 0.05)
 
-				&:hover
-					background rgba(0, 0, 0, 0.05)
+			&:active
+				background rgba(0, 0, 0, 0.1)
 
-				&:active
-					background rgba(0, 0, 0, 0.1)
+			> .backdrop
+				position absolute
+				top 0
+				left 0
+				height 100%
+				background $theme-color
+				transition width 1s ease
 
-				> .backdrop
-					position absolute
-					top 0
-					left 0
-					height 100%
-					background $theme-color
-					transition width 1s ease
+			> .votes
+				margin-left 4px
 
-				> .votes
-					margin-left 4px
+	> p
+		a
+			color inherit
 
-		> p
-			a
-				color inherit
+	&[data-is-voted]
+		> ul > li
+			cursor default
 
-		&[data-is-voted]
-			> ul > li
-				cursor default
+			&:hover
+				background transparent
 
-				&:hover
-					background transparent
-
-				&:active
-					background transparent
+			&:active
+				background transparent
 
 </style>
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 6b2d75d84..eabcc485d 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -287,7 +287,7 @@ export default Vue.extend({
 			width calc(100% - 275px * 2)
 			order 2
 
-		> *:not(main)
+		> *:not(.main)
 			width 275px
 			padding 16px 0 16px 0
 
@@ -303,7 +303,7 @@ export default Vue.extend({
 			order 3
 
 		@media (max-width 1100px)
-			> *:not(main)
+			> *:not(.main)
 				display none
 
 			> .main
diff --git a/src/web/app/desktop/views/components/notifications.vue b/src/web/app/desktop/views/components/notifications.vue
index 443ebea2a..e3a69d620 100644
--- a/src/web/app/desktop/views/components/notifications.vue
+++ b/src/web/app/desktop/views/components/notifications.vue
@@ -10,7 +10,7 @@
 					</a>
 					<div class="text">
 						<p>
-							<mk-reaction-icon reaction={ notification.reaction }/>
+							<mk-reaction-icon :reaction="notification.reaction"/>
 							<a :href="`/${notification.user.username}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a>
 						</p>
 						<a class="post-ref" :href="`/${notification.post.user.username}/${notification.post.id}`">
diff --git a/src/web/app/desktop/views/components/posts.vue b/src/web/app/desktop/views/components/posts.vue
index 7576fd31b..ec36889ec 100644
--- a/src/web/app/desktop/views/components/posts.vue
+++ b/src/web/app/desktop/views/components/posts.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-posts">
 	<template v-for="(post, i) in _posts">
-		<x-post :post.sync="post" :key="post.id"/>
+		<x-post :post="post" :key="post.id" @update:post="onPostUpdated(i, $event)"/>
 		<p class="date" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date">
 			<span>%fa:angle-up%{{ post._datetext }}</span>
 			<span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span>
@@ -41,6 +41,9 @@ export default Vue.extend({
 	methods: {
 		focus() {
 			(this.$el as any).children[0].focus();
+		},
+		onPostUpdated(i, post) {
+			Vue.set((this as any).posts, i, post);
 		}
 	}
 });
diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index f5e4ce48f..a2a87807d 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -13,6 +13,7 @@ import userCard from './user-card.vue';
 import postDetail from './post-detail.vue';
 import followButton from './follow-button.vue';
 import friendsMaker from './friends-maker.vue';
+import notification from './notification.vue';
 import notifications from './notifications.vue';
 import notificationPreview from './notification-preview.vue';
 
@@ -29,5 +30,6 @@ Vue.component('mk-user-card', userCard);
 Vue.component('mk-post-detail', postDetail);
 Vue.component('mk-follow-button', followButton);
 Vue.component('mk-friends-maker', friendsMaker);
+Vue.component('mk-notification', notification);
 Vue.component('mk-notifications', notifications);
 Vue.component('mk-notification-preview', notificationPreview);
diff --git a/src/web/app/mobile/views/components/notification.vue b/src/web/app/mobile/views/components/notification.vue
index 98390f1c1..dce373b45 100644
--- a/src/web/app/mobile/views/components/notification.vue
+++ b/src/web/app/mobile/views/components/notification.vue
@@ -106,6 +106,7 @@ import Vue from 'vue';
 import getPostSummary from '../../../../../common/get-post-summary';
 
 export default Vue.extend({
+	props: ['notification'],
 	data() {
 		return {
 			getPostSummary
diff --git a/src/web/app/mobile/views/components/notifications.vue b/src/web/app/mobile/views/components/notifications.vue
index 99083ed4b..1cd6e2bc1 100644
--- a/src/web/app/mobile/views/components/notifications.vue
+++ b/src/web/app/mobile/views/components/notifications.vue
@@ -10,7 +10,8 @@
 		</template>
 	</div>
 	<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
-		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-notifications.more%' }
+		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>
+		{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:mobile.tags.mk-notifications.more%' }}
 	</button>
 	<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:mobile.tags.mk-notifications.empty%</p>
 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
diff --git a/src/web/app/mobile/views/components/posts.vue b/src/web/app/mobile/views/components/posts.vue
index b028264b5..34fb0749a 100644
--- a/src/web/app/mobile/views/components/posts.vue
+++ b/src/web/app/mobile/views/components/posts.vue
@@ -3,7 +3,7 @@
 	<slot name="head"></slot>
 	<slot></slot>
 	<template v-for="(post, i) in _posts">
-		<x-post :post="post" :key="post.id"/>
+		<x-post :post="post" :key="post.id" @update:post="onPostUpdated(i, $event)"/>
 		<p class="date" v-if="i != posts.length - 1 && post._date != _posts[i + 1]._date">
 			<span>%fa:angle-up%{{ post._datetext }}</span>
 			<span>%fa:angle-down%{{ _posts[i + 1]._datetext }}</span>
@@ -39,6 +39,11 @@ export default Vue.extend({
 				return post;
 			});
 		}
+	},
+	methods: {
+		onPostUpdated(i, post) {
+			Vue.set((this as any).posts, i, post);
+		}
 	}
 });
 </script>
diff --git a/src/web/app/mobile/views/pages/post.vue b/src/web/app/mobile/views/pages/post.vue
index c62a001f2..2ed2ebfcf 100644
--- a/src/web/app/mobile/views/pages/post.vue
+++ b/src/web/app/mobile/views/pages/post.vue
@@ -4,7 +4,7 @@
 	<main v-if="!fetching">
 		<a v-if="post.next" :href="post.next">%fa:angle-up%%i18n:mobile.tags.mk-post-page.next%</a>
 		<div>
-			<mk-post-detail :post="parent.post"/>
+			<mk-post-detail :post="post"/>
 		</div>
 		<a v-if="post.prev" :href="post.prev">%fa:angle-down%%i18n:mobile.tags.mk-post-page.prev%</a>
 	</main>
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index 335b2bc1e..c9c1c6bfb 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -58,7 +58,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import age from 's-age';
+import * as age from 's-age';
 import Progress from '../../../common/scripts/loading';
 import XHome from './user/home.vue';
 

From 29b7bd258c0f31e456fef2148318679c31cfddd9 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 22:27:28 +0900
Subject: [PATCH 277/286] wip

---
 src/web/app/common/-tags/introduction.tag     |  25 ----
 .../app/common/views/components/post-html.ts  |   2 +-
 .../views/components/twitter-setting.vue      |   2 +-
 .../views/components/posts.post.sub.vue       | 121 +++++++++---------
 .../desktop/views/components/posts.post.vue   |   2 +-
 .../views/components/widgets/broadcast.vue    |   2 +-
 .../components/widgets/channel.channel.vue    |   8 +-
 .../views/components/widgets/channel.vue      |   2 +-
 .../views/components/posts.post.sub.vue       |   2 +-
 9 files changed, 70 insertions(+), 96 deletions(-)
 delete mode 100644 src/web/app/common/-tags/introduction.tag

diff --git a/src/web/app/common/-tags/introduction.tag b/src/web/app/common/-tags/introduction.tag
deleted file mode 100644
index c92cff0d1..000000000
--- a/src/web/app/common/-tags/introduction.tag
+++ /dev/null
@@ -1,25 +0,0 @@
-<mk-introduction>
-	<article>
-		<h1>Misskeyとは?</h1>
-		<p><ruby>Misskey<rt>みすきー</rt></ruby>は、<a href="http://syuilo.com" target="_blank">syuilo</a>が2014年くらいから<a href="https://github.com/syuilo/misskey" target="_blank">オープンソースで</a>開発・運営を行っている、ミニブログベースのSNSです。</p>
-		<p>無料で誰でも利用でき、広告も掲載していません。</p>
-		<p><a href={ _DOCS_URL_ } target="_blank">もっと知りたい方はこちら</a></p>
-	</article>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			h1
-				margin 0
-				text-align center
-				font-size 1.2em
-
-			p
-				margin 16px 0
-
-				&:last-child
-					margin 0
-					text-align center
-
-	</style>
-</mk-introduction>
diff --git a/src/web/app/common/views/components/post-html.ts b/src/web/app/common/views/components/post-html.ts
index d365bdc49..afd95f8e3 100644
--- a/src/web/app/common/views/components/post-html.ts
+++ b/src/web/app/common/views/components/post-html.ts
@@ -93,6 +93,6 @@ export default Vue.component('mk-post-html', {
 			}
 		}));
 
-		return createElement('div', els);
+		return createElement('span', els);
 	}
 });
diff --git a/src/web/app/common/views/components/twitter-setting.vue b/src/web/app/common/views/components/twitter-setting.vue
index 996f34fb7..aaca6ccdd 100644
--- a/src/web/app/common/views/components/twitter-setting.vue
+++ b/src/web/app/common/views/components/twitter-setting.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-twitter-setting">
 	<p>%i18n:common.tags.mk-twitter-setting.description%<a :href="`${docsUrl}/link-to-twitter`" target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p>
-	<p class="account" v-if="os.i.twitter" :title="`Twitter ID: ${os.i.twitter.user_id}`">%i18n:common.tags.mk-twitter-setting.connected-to%: <a :href="`https://twitter.com/${os.i.twitter.screen_name}`" target="_blank">@{{ I.twitter.screen_name }}</a></p>
+	<p class="account" v-if="os.i.twitter" :title="`Twitter ID: ${os.i.twitter.user_id}`">%i18n:common.tags.mk-twitter-setting.connected-to%: <a :href="`https://twitter.com/${os.i.twitter.screen_name}`" target="_blank">@{{ os.i.twitter.screen_name }}</a></p>
 	<p>
 		<a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }}</a>
 		<span v-if="os.i.twitter"> or </span>
diff --git a/src/web/app/desktop/views/components/posts.post.sub.vue b/src/web/app/desktop/views/components/posts.post.sub.vue
index 4e52d1d70..f92077516 100644
--- a/src/web/app/desktop/views/components/posts.post.sub.vue
+++ b/src/web/app/desktop/views/components/posts.post.sub.vue
@@ -35,77 +35,74 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 .sub
 	margin 0
-	padding 0
+	padding 16px
 	font-size 0.9em
 
-	> article
-		padding 16px
+	&:after
+		content ""
+		display block
+		clear both
 
-		&:after
-			content ""
+	&:hover
+		> .main > footer > button
+			color #888
+
+	> .avatar-anchor
+		display block
+		float left
+		margin 0 14px 0 0
+
+		> .avatar
 			display block
-			clear both
+			width 52px
+			height 52px
+			margin 0
+			border-radius 8px
+			vertical-align bottom
 
-		&:hover
-			> .main > footer > button
-				color #888
+	> .main
+		float left
+		width calc(100% - 66px)
 
-		> .avatar-anchor
-			display block
-			float left
-			margin 0 14px 0 0
+		> header
+			display flex
+			margin-bottom 2px
+			white-space nowrap
+			line-height 21px
 
-			> .avatar
+			> .name
 				display block
-				width 52px
-				height 52px
+				margin 0 .5em 0 0
+				padding 0
+				overflow hidden
+				color #607073
+				font-size 1em
+				font-weight bold
+				text-decoration none
+				text-overflow ellipsis
+
+				&:hover
+					text-decoration underline
+
+			> .username
+				margin 0 .5em 0 0
+				color #d1d8da
+
+			> .created-at
+				margin-left auto
+				color #b2b8bb
+
+		> .body
+
+			> .text
+				cursor default
 				margin 0
-				border-radius 8px
-				vertical-align bottom
+				padding 0
+				font-size 1.1em
+				color #717171
 
-		> .main
-			float left
-			width calc(100% - 66px)
-
-			> header
-				display flex
-				margin-bottom 2px
-				white-space nowrap
-				line-height 21px
-
-				> .name
-					display block
-					margin 0 .5em 0 0
-					padding 0
-					overflow hidden
-					color #607073
-					font-size 1em
-					font-weight bold
-					text-decoration none
-					text-overflow ellipsis
-
-					&:hover
-						text-decoration underline
-
-				> .username
-					margin 0 .5em 0 0
-					color #d1d8da
-
-				> .created-at
-					margin-left auto
-					color #b2b8bb
-
-			> .body
-
-				> .text
-					cursor default
-					margin 0
-					padding 0
-					font-size 1.1em
-					color #717171
-
-					pre
-						max-height 120px
-						font-size 80%
+				pre
+					max-height 120px
+					font-size 80%
 
 </style>
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index c757cbc7f..24f7b5e12 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="post" tabindex="-1" :title="title" @keydown="onKeydown">
 	<div class="reply-to" v-if="p.reply">
-		<x-sub post="p.reply"/>
+		<x-sub :post="p.reply"/>
 	</div>
 	<div class="repost" v-if="isRepost">
 		<p>
diff --git a/src/web/app/desktop/views/components/widgets/broadcast.vue b/src/web/app/desktop/views/components/widgets/broadcast.vue
index 68c9cebfa..e4b7e2532 100644
--- a/src/web/app/desktop/views/components/widgets/broadcast.vue
+++ b/src/web/app/desktop/views/components/widgets/broadcast.vue
@@ -12,7 +12,7 @@
 	<p class="fetching" v-if="fetching">%i18n:desktop.tags.mk-broadcast-home-widget.fetching%<mk-ellipsis/></p>
 	<h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:desktop.tags.mk-broadcast-home-widget.no-broadcasts%' : broadcasts[i].title }}</h1>
 	<p v-if="!fetching">
-		<span v-if="broadcasts.length != 0" :v-html="broadcasts[i].text"></span>
+		<span v-if="broadcasts.length != 0" v-html="broadcasts[i].text"></span>
 		<template v-if="broadcasts.length == 0">%i18n:desktop.tags.mk-broadcast-home-widget.have-a-nice-day%</template>
 	</p>
 	<a v-if="broadcasts.length > 1" @click="next">%i18n:desktop.tags.mk-broadcast-home-widget.next% &gt;&gt;</a>
diff --git a/src/web/app/desktop/views/components/widgets/channel.channel.vue b/src/web/app/desktop/views/components/widgets/channel.channel.vue
index 5de13aec0..a28b4aeb9 100644
--- a/src/web/app/desktop/views/components/widgets/channel.channel.vue
+++ b/src/web/app/desktop/views/components/widgets/channel.channel.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="channel">
 	<p v-if="fetching">読み込み中<mk-ellipsis/></p>
-	<div v-if="!fetching" ref="posts">
+	<div v-if="!fetching" ref="posts" class="posts">
 		<p v-if="posts.length == 0">まだ投稿がありません</p>
 		<x-post class="post" v-for="post in posts.slice().reverse()" :post="post" :key="post.id" @reply="reply"/>
 	</div>
@@ -34,7 +34,9 @@ export default Vue.extend({
 		}
 	},
 	mounted() {
-		this.zap();
+		this.$nextTick(() => {
+			this.zap();
+		});
 	},
 	beforeDestroy() {
 		this.disconnect();
@@ -85,7 +87,7 @@ export default Vue.extend({
 		text-align center
 		color #aaa
 
-	> div
+	> .posts
 		height calc(100% - 38px)
 		overflow auto
 		font-size 0.9em
diff --git a/src/web/app/desktop/views/components/widgets/channel.vue b/src/web/app/desktop/views/components/widgets/channel.vue
index 1b98be734..5c3afd9ec 100644
--- a/src/web/app/desktop/views/components/widgets/channel.vue
+++ b/src/web/app/desktop/views/components/widgets/channel.vue
@@ -5,7 +5,7 @@
 		<button @click="settings" title="%i18n:desktop.tags.mk-channel-home-widget.settings%">%fa:cog%</button>
 	</template>
 	<p class="get-started" v-if="props.channel == null">%i18n:desktop.tags.mk-channel-home-widget.get-started%</p>
-	<x-channel class="channel" :channel="channel" v-else/>
+	<x-channel class="channel" :channel="channel" v-if="channel != null"/>
 </div>
 </template>
 
diff --git a/src/web/app/mobile/views/components/posts.post.sub.vue b/src/web/app/mobile/views/components/posts.post.sub.vue
index 5bb6444a6..f1c858675 100644
--- a/src/web/app/mobile/views/components/posts.post.sub.vue
+++ b/src/web/app/mobile/views/components/posts.post.sub.vue
@@ -7,7 +7,7 @@
 		<header>
 			<router-link class="name" :to="`/${post.user.username}`">{{ post.user.name }}</router-link>
 			<span class="username">@{{ post.user.username }}</span>
-			<router-link class="created-at" :href="`/${post.user.username}/${post.id}`">
+			<router-link class="created-at" :to="`/${post.user.username}/${post.id}`">
 				<mk-time :time="post.created_at"/>
 			</router-link>
 		</header>

From e651bd12c3a07436f1986b0634a508100a66ef85 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 22:38:14 +0900
Subject: [PATCH 278/286] wip

---
 src/web/app/mobile/script.ts                  |  4 ++++
 .../mobile/views/pages/profile-setting.vue    | 22 ++++++++++++-------
 src/web/app/mobile/views/pages/settings.vue   |  6 ++---
 3 files changed, 21 insertions(+), 11 deletions(-)

diff --git a/src/web/app/mobile/script.ts b/src/web/app/mobile/script.ts
index 6e69b3ed3..fe73155c7 100644
--- a/src/web/app/mobile/script.ts
+++ b/src/web/app/mobile/script.ts
@@ -26,6 +26,8 @@ import MkPost from './views/pages/post.vue';
 import MkSearch from './views/pages/search.vue';
 import MkFollowers from './views/pages/followers.vue';
 import MkFollowing from './views/pages/following.vue';
+import MkSettings from './views/pages/settings.vue';
+import MkProfileSetting from './views/pages/profile-setting.vue';
 
 /**
  * init
@@ -54,6 +56,8 @@ init((launch) => {
 	app.$router.addRoutes([
 		{ path: '/', name: 'index', component: MkIndex },
 		{ path: '/signup', name: 'signup', component: MkSignup },
+		{ path: '/i/settings', component: MkSettings },
+		{ path: '/i/settings/profile', component: MkProfileSetting },
 		{ path: '/i/notifications', component: MkNotifications },
 		{ path: '/i/messaging', component: MkMessaging },
 		{ path: '/i/messaging/:username', component: MkMessagingRoom },
diff --git a/src/web/app/mobile/views/pages/profile-setting.vue b/src/web/app/mobile/views/pages/profile-setting.vue
index 3b93496a3..432a850e4 100644
--- a/src/web/app/mobile/views/pages/profile-setting.vue
+++ b/src/web/app/mobile/views/pages/profile-setting.vue
@@ -1,9 +1,9 @@
 <template>
 <mk-ui>
 	<span slot="header">%fa:user%%i18n:mobile.tags.mk-profile-setting-page.title%</span>
-	<div class="$style.content">
+	<div :class="$style.content">
 		<p>%fa:info-circle%%i18n:mobile.tags.mk-profile-setting.will-be-published%</p>
-		<div class="$style.form">
+		<div :class="$style.form">
 			<div :style="os.i.banner_url ? `background-image: url(${os.i.banner_url}?thumbnail&size=1024)` : ''" @click="setBanner">
 				<img :src="`${os.i.avatar_url}?thumbnail&size=200`" alt="avatar" @click="setAvatar"/>
 			</div>
@@ -32,7 +32,7 @@
 				<button @click="setBanner" :disabled="bannerSaving">%i18n:mobile.tags.mk-profile-setting.set-banner%</button>
 			</label>
 		</div>
-		<button class="$style.save" @click="save" :disabled="saving">%fa:check%%i18n:mobile.tags.mk-profile-setting.save%</button>
+		<button :class="$style.save" @click="save" :disabled="saving">%fa:check%%i18n:mobile.tags.mk-profile-setting.save%</button>
 	</div>
 </mk-ui>
 </template>
@@ -42,15 +42,21 @@ import Vue from 'vue';
 export default Vue.extend({
 	data() {
 		return {
-			name: (this as any).os.i.name,
-			location: (this as any).os.i.profile.location,
-			description: (this as any).os.i.description,
-			birthday: (this as any).os.i.profile.birthday,
+			name: null,
+			location: null,
+			description: null,
+			birthday: null,
 			avatarSaving: false,
 			bannerSaving: false,
 			saving: false
 		};
 	},
+	created() {
+		this.name = (this as any).os.i.name;
+		this.location = (this as any).os.i.profile.location;
+		this.description = (this as any).os.i.description;
+		this.birthday = (this as any).os.i.profile.birthday;
+	},
 	mounted() {
 		document.title = 'Misskey | %i18n:mobile.tags.mk-profile-setting-page.title%';
 		document.documentElement.style.background = '#313a42';
@@ -101,7 +107,7 @@ export default Vue.extend({
 });
 </script>
 
-<style lang="stylus" scoped>
+<style lang="stylus" module>
 .content
 	margin 8px auto
 	max-width 500px
diff --git a/src/web/app/mobile/views/pages/settings.vue b/src/web/app/mobile/views/pages/settings.vue
index a3d5dd92e..3250999e1 100644
--- a/src/web/app/mobile/views/pages/settings.vue
+++ b/src/web/app/mobile/views/pages/settings.vue
@@ -1,10 +1,10 @@
 <template>
 <mk-ui>
 	<span slot="header">%fa:cog%%i18n:mobile.tags.mk-settings-page.settings%</span>
-	<div class="$style.content">
+	<div :class="$style.content">
 		<p v-html="'%i18n:mobile.tags.mk-settings.signed-in-as%'.replace('{}', '<b>' + os.i.name + '</b>')"></p>
 		<ul>
-			<li><router-link to="./settings/profile">%fa:user%%i18n:mobile.tags.mk-settings-page.profile%%fa:angle-right%</a></li>
+			<li><router-link to="./settings/profile">%fa:user%%i18n:mobile.tags.mk-settings-page.profile%%fa:angle-right%</router-link></li>
 			<li><router-link to="./settings/authorized-apps">%fa:puzzle-piece%%i18n:mobile.tags.mk-settings-page.applications%%fa:angle-right%</router-link></li>
 			<li><router-link to="./settings/twitter">%fa:B twitter%%i18n:mobile.tags.mk-settings-page.twitter-integration%%fa:angle-right%</router-link></li>
 			<li><router-link to="./settings/signin-history">%fa:sign-in-alt%%i18n:mobile.tags.mk-settings-page.signin-history%%fa:angle-right%</router-link></li>
@@ -19,7 +19,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { version } from '../../../../config';
+import { version } from '../../../config';
 
 export default Vue.extend({
 	data() {

From 4b228432c1397c2fc27b96c3f172cb36018f1b9b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 22:51:33 +0900
Subject: [PATCH 279/286] wip

---
 src/web/app/mobile/views/components/index.ts  |  4 ++
 .../views/components/user-followers.vue       | 26 ---------
 .../views/components/user-following.vue       | 26 ---------
 .../mobile/views/components/user-preview.vue  |  6 +-
 .../mobile/views/components/users-list.vue    |  7 ++-
 src/web/app/mobile/views/pages/followers.vue  | 54 +++++++++++++-----
 src/web/app/mobile/views/pages/following.vue  | 56 +++++++++++++------
 7 files changed, 92 insertions(+), 87 deletions(-)
 delete mode 100644 src/web/app/mobile/views/components/user-followers.vue
 delete mode 100644 src/web/app/mobile/views/components/user-following.vue

diff --git a/src/web/app/mobile/views/components/index.ts b/src/web/app/mobile/views/components/index.ts
index a2a87807d..73cc1f9f3 100644
--- a/src/web/app/mobile/views/components/index.ts
+++ b/src/web/app/mobile/views/components/index.ts
@@ -16,6 +16,8 @@ import friendsMaker from './friends-maker.vue';
 import notification from './notification.vue';
 import notifications from './notifications.vue';
 import notificationPreview from './notification-preview.vue';
+import usersList from './users-list.vue';
+import userPreview from './user-preview.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-home', home);
@@ -33,3 +35,5 @@ Vue.component('mk-friends-maker', friendsMaker);
 Vue.component('mk-notification', notification);
 Vue.component('mk-notifications', notifications);
 Vue.component('mk-notification-preview', notificationPreview);
+Vue.component('mk-users-list', usersList);
+Vue.component('mk-user-preview', userPreview);
diff --git a/src/web/app/mobile/views/components/user-followers.vue b/src/web/app/mobile/views/components/user-followers.vue
deleted file mode 100644
index 771291b49..000000000
--- a/src/web/app/mobile/views/components/user-followers.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<template>
-<mk-users-list
-	:fetch="fetch"
-	:count="user.followers_count"
-	:you-know-count="user.followers_you_know_count"
->
-	%i18n:mobile.tags.mk-user-followers.no-users%
-</mk-users-list>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	props: ['user'],
-	methods: {
-		fetch(iknow, limit, cursor, cb) {
-			(this as any).api('users/followers', {
-				user_id: this.user.id,
-				iknow: iknow,
-				limit: limit,
-				cursor: cursor ? cursor : undefined
-			}).then(cb);
-		}
-	}
-});
-</script>
diff --git a/src/web/app/mobile/views/components/user-following.vue b/src/web/app/mobile/views/components/user-following.vue
deleted file mode 100644
index dfd6135da..000000000
--- a/src/web/app/mobile/views/components/user-following.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<template>
-<mk-users-list
-	:fetch="fetch"
-	:count="user.following_count"
-	:you-know-count="user.following_you_know_count"
->
-	%i18n:mobile.tags.mk-user-following.no-users%
-</mk-users-list>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	props: ['user'],
-	methods: {
-		fetch(iknow, limit, cursor, cb) {
-			(this as any).api('users/following', {
-				user_id: this.user.id,
-				iknow: iknow,
-				limit: limit,
-				cursor: cursor ? cursor : undefined
-			}).then(cb);
-		}
-	}
-});
-</script>
diff --git a/src/web/app/mobile/views/components/user-preview.vue b/src/web/app/mobile/views/components/user-preview.vue
index 0246cac6a..3cbc20033 100644
--- a/src/web/app/mobile/views/components/user-preview.vue
+++ b/src/web/app/mobile/views/components/user-preview.vue
@@ -1,11 +1,11 @@
 <template>
 <div class="mk-user-preview">
-	<a class="avatar-anchor" :href="`/${user.username}`">
+	<router-link class="avatar-anchor" :to="`/${user.username}`">
 		<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
-	</a>
+	</router-link>
 	<div class="main">
 		<header>
-			<a class="name" :href="`/${user.username}`">{{ user.name }}</a>
+			<router-link class="name" :to="`/${user.username}`">{{ user.name }}</router-link>
 			<span class="username">@{{ user.username }}</span>
 		</header>
 		<div class="body">
diff --git a/src/web/app/mobile/views/components/users-list.vue b/src/web/app/mobile/views/components/users-list.vue
index 24c96aec7..d6c626135 100644
--- a/src/web/app/mobile/views/components/users-list.vue
+++ b/src/web/app/mobile/views/components/users-list.vue
@@ -32,13 +32,18 @@ export default Vue.extend({
 			next: null
 		};
 	},
+	watch: {
+		mode() {
+			this._fetch();
+		}
+	},
 	mounted() {
 		this._fetch(() => {
 			this.$emit('loaded');
 		});
 	},
 	methods: {
-		_fetch(cb) {
+		_fetch(cb?) {
 			this.fetching = true;
 			this.fetch(this.mode == 'iknow', this.limit, null, obj => {
 				this.users = obj.users;
diff --git a/src/web/app/mobile/views/pages/followers.vue b/src/web/app/mobile/views/pages/followers.vue
index 2f102bd68..c2b6b90e2 100644
--- a/src/web/app/mobile/views/pages/followers.vue
+++ b/src/web/app/mobile/views/pages/followers.vue
@@ -1,10 +1,18 @@
 <template>
 <mk-ui>
-	<span slot="header" v-if="!fetching">
+	<template slot="header" v-if="!fetching">
 		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt="">
 		{{ '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) }}
-	</span>
-	<mk-user-followers v-if="!fetching" :user="user" @loaded="onLoaded"/>
+	</template>
+	<mk-users-list
+		v-if="!fetching"
+		:fetch="fetchUsers"
+		:count="user.followers_count"
+		:you-know-count="user.followers_you_know_count"
+		@loaded="onLoaded"
+	>
+		%i18n:mobile.tags.mk-user-followers.no-users%
+	</mk-users-list>
 </mk-ui>
 </template>
 
@@ -13,29 +21,45 @@ import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
-	props: ['username'],
 	data() {
 		return {
 			fetching: true,
 			user: null
 		};
 	},
+	watch: {
+		$route: 'fetch'
+	},
+	created() {
+		this.fetch();
+	},
 	mounted() {
-		Progress.start();
-
-		(this as any).api('users/show', {
-			username: this.username
-		}).then(user => {
-			this.user = user;
-			this.fetching = false;
-
-			document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
-			document.documentElement.style.background = '#313a42';
-		});
+		document.documentElement.style.background = '#313a42';
 	},
 	methods: {
+		fetch() {
+			Progress.start();
+			this.fetching = true;
+
+			(this as any).api('users/show', {
+				username: this.$route.params.user
+			}).then(user => {
+				this.user = user;
+				this.fetching = false;
+
+				document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
+			});
+		},
 		onLoaded() {
 			Progress.done();
+		},
+		fetchUsers(iknow, limit, cursor, cb) {
+			(this as any).api('users/followers', {
+				user_id: this.user.id,
+				iknow: iknow,
+				limit: limit,
+				cursor: cursor ? cursor : undefined
+			}).then(cb);
 		}
 	}
 });
diff --git a/src/web/app/mobile/views/pages/following.vue b/src/web/app/mobile/views/pages/following.vue
index 20f085a9f..6365d3b37 100644
--- a/src/web/app/mobile/views/pages/following.vue
+++ b/src/web/app/mobile/views/pages/following.vue
@@ -1,10 +1,18 @@
 <template>
 <mk-ui>
-	<span slot="header" v-if="!fetching">
+	<template slot="header" v-if="!fetching">
 		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt="">
-		{{ '%i18n:mobile.tags.mk-user-following-page.following-of'.replace('{}', user.name) }}
-	</span>
-	<mk-user-following v-if="!fetching" :user="user" @loaded="onLoaded"/>
+		{{ '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', user.name) }}
+	</template>
+	<mk-users-list
+		v-if="!fetching"
+		:fetch="fetchUsers"
+		:count="user.following_count"
+		:you-know-count="user.following_you_know_count"
+		@loaded="onLoaded"
+	>
+		%i18n:mobile.tags.mk-user-following.no-users%
+	</mk-users-list>
 </mk-ui>
 </template>
 
@@ -13,29 +21,45 @@ import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
-	props: ['username'],
 	data() {
 		return {
 			fetching: true,
 			user: null
 		};
 	},
+	watch: {
+		$route: 'fetch'
+	},
+	created() {
+		this.fetch();
+	},
 	mounted() {
-		Progress.start();
-
-		(this as any).api('users/show', {
-			username: this.username
-		}).then(user => {
-			this.user = user;
-			this.fetching = false;
-
-			document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
-			document.documentElement.style.background = '#313a42';
-		});
+		document.documentElement.style.background = '#313a42';
 	},
 	methods: {
+		fetch() {
+			Progress.start();
+			this.fetching = true;
+
+			(this as any).api('users/show', {
+				username: this.$route.params.user
+			}).then(user => {
+				this.user = user;
+				this.fetching = false;
+
+				document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
+			});
+		},
 		onLoaded() {
 			Progress.done();
+		},
+		fetchUsers(iknow, limit, cursor, cb) {
+			(this as any).api('users/following', {
+				user_id: this.user.id,
+				iknow: iknow,
+				limit: limit,
+				cursor: cursor ? cursor : undefined
+			}).then(cb);
 		}
 	}
 });

From e98626dbbcf026865a24fb46275c6880a721b42f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 23:05:05 +0900
Subject: [PATCH 280/286] wip

---
 .../app/desktop/views/components/followers-window.vue |  2 +-
 .../components/{user-followers.vue => followers.vue}  |  0
 .../app/desktop/views/components/following-window.vue |  2 +-
 .../components/{user-following.vue => following.vue}  |  0
 src/web/app/desktop/views/components/index.ts         |  6 ++++++
 .../components/{list-user.vue => users-list.item.vue} | 11 +++++------
 src/web/app/desktop/views/components/users-list.vue   |  7 ++++++-
 src/web/app/desktop/views/pages/user/user.profile.vue |  4 +++-
 8 files changed, 22 insertions(+), 10 deletions(-)
 rename src/web/app/desktop/views/components/{user-followers.vue => followers.vue} (100%)
 rename src/web/app/desktop/views/components/{user-following.vue => following.vue} (100%)
 rename src/web/app/desktop/views/components/{list-user.vue => users-list.item.vue} (86%)

diff --git a/src/web/app/desktop/views/components/followers-window.vue b/src/web/app/desktop/views/components/followers-window.vue
index ed439114c..d41d356f9 100644
--- a/src/web/app/desktop/views/components/followers-window.vue
+++ b/src/web/app/desktop/views/components/followers-window.vue
@@ -3,7 +3,7 @@
 	<span slot="header" :class="$style.header">
 		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロワー
 	</span>
-	<mk-followers-list :user="user"/>
+	<mk-followers :user="user"/>
 </mk-window>
 </template>
 
diff --git a/src/web/app/desktop/views/components/user-followers.vue b/src/web/app/desktop/views/components/followers.vue
similarity index 100%
rename from src/web/app/desktop/views/components/user-followers.vue
rename to src/web/app/desktop/views/components/followers.vue
diff --git a/src/web/app/desktop/views/components/following-window.vue b/src/web/app/desktop/views/components/following-window.vue
index 4e1fb0306..c516b3b17 100644
--- a/src/web/app/desktop/views/components/following-window.vue
+++ b/src/web/app/desktop/views/components/following-window.vue
@@ -3,7 +3,7 @@
 	<span slot="header" :class="$style.header">
 		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロー
 	</span>
-	<mk-following-list :user="user"/>
+	<mk-following :user="user"/>
 </mk-window>
 </template>
 
diff --git a/src/web/app/desktop/views/components/user-following.vue b/src/web/app/desktop/views/components/following.vue
similarity index 100%
rename from src/web/app/desktop/views/components/user-following.vue
rename to src/web/app/desktop/views/components/following.vue
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 0e4629172..fc30bb729 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -27,6 +27,9 @@ import settings from './settings.vue';
 import calendar from './calendar.vue';
 import activity from './activity.vue';
 import friendsMaker from './friends-maker.vue';
+import followers from './followers.vue';
+import following from './following.vue';
+import usersList from './users-list.vue';
 import wNav from './widgets/nav.vue';
 import wCalendar from './widgets/calendar.vue';
 import wPhotoStream from './widgets/photo-stream.vue';
@@ -76,6 +79,9 @@ Vue.component('mk-settings', settings);
 Vue.component('mk-calendar', calendar);
 Vue.component('mk-activity', activity);
 Vue.component('mk-friends-maker', friendsMaker);
+Vue.component('mk-followers', followers);
+Vue.component('mk-following', following);
+Vue.component('mk-users-list', usersList);
 Vue.component('mkw-nav', wNav);
 Vue.component('mkw-calendar', wCalendar);
 Vue.component('mkw-photo-stream', wPhotoStream);
diff --git a/src/web/app/desktop/views/components/list-user.vue b/src/web/app/desktop/views/components/users-list.item.vue
similarity index 86%
rename from src/web/app/desktop/views/components/list-user.vue
rename to src/web/app/desktop/views/components/users-list.item.vue
index adaa8f092..374f55b41 100644
--- a/src/web/app/desktop/views/components/list-user.vue
+++ b/src/web/app/desktop/views/components/users-list.item.vue
@@ -1,11 +1,11 @@
 <template>
-<div class="mk-list-user">
-	<a class="avatar-anchor" :href="`/${user.username}`">
+<div class="root item">
+	<router-link class="avatar-anchor" :to="`/${user.username}`" v-user-preview="user.id">
 		<img class="avatar" :src="`${user.avatar_url}?thumbnail&size=64`" alt="avatar"/>
-	</a>
+	</router-link>
 	<div class="main">
 		<header>
-			<a class="name" :href="`/${user.username}`">{{ user.name }}</a>
+			<router-link class="name" :to="`/${user.username}`" v-user-preview="user.id">{{ user.name }}</router-link>
 			<span class="username">@{{ user.username }}</span>
 		</header>
 		<div class="body">
@@ -25,8 +25,7 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-list-user
-	margin 0
+.root.item
 	padding 16px
 	font-size 16px
 
diff --git a/src/web/app/desktop/views/components/users-list.vue b/src/web/app/desktop/views/components/users-list.vue
index b93a81630..fd15f478d 100644
--- a/src/web/app/desktop/views/components/users-list.vue
+++ b/src/web/app/desktop/views/components/users-list.vue
@@ -8,7 +8,7 @@
 	</nav>
 	<div class="users" v-if="!fetching && users.length != 0">
 		<div v-for="u in users" :key="u.id">
-			<mk-list-user :user="u"/>
+			<x-item :user="u"/>
 		</div>
 	</div>
 	<button class="more" v-if="!fetching && next != null" @click="more" :disabled="moreFetching">
@@ -24,7 +24,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import XItem from './users-list.item.vue';
+
 export default Vue.extend({
+	components: {
+		XItem
+	},
 	props: ['fetch', 'count', 'youKnowCount'],
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/pages/user/user.profile.vue b/src/web/app/desktop/views/pages/user/user.profile.vue
index db2e32e80..b55787c95 100644
--- a/src/web/app/desktop/views/pages/user/user.profile.vue
+++ b/src/web/app/desktop/views/pages/user/user.profile.vue
@@ -23,7 +23,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import age from 's-age';
+import * as age from 's-age';
 import MkFollowingWindow from '../../components/following-window.vue';
 import MkFollowersWindow from '../../components/followers-window.vue';
 
@@ -37,6 +37,7 @@ export default Vue.extend({
 	methods: {
 		showFollowing() {
 			document.body.appendChild(new MkFollowingWindow({
+				parent: this,
 				propsData: {
 					user: this.user
 				}
@@ -45,6 +46,7 @@ export default Vue.extend({
 
 		showFollowers() {
 			document.body.appendChild(new MkFollowersWindow({
+				parent: this,
 				propsData: {
 					user: this.user
 				}

From b5068aae059c50ef837d2a5a20ae6c85d8ee7f98 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 22 Feb 2018 23:53:07 +0900
Subject: [PATCH 281/286] wip

---
 src/web/app/common/mios.ts                    | 10 ++++++
 .../app/desktop/views/components/drive.vue    |  8 ++---
 .../desktop/views/components/images-image.vue |  8 ++---
 .../views/components/messaging-window.vue     |  8 ++---
 .../desktop/views/components/post-detail.vue  | 36 ++++++++-----------
 .../desktop/views/components/posts.post.vue   | 36 ++++++++-----------
 .../views/components/ui.header.account.vue    |  4 +--
 .../views/components/ui.header.nav.vue        |  2 +-
 .../views/components/widgets/messaging.vue    |  8 ++---
 .../desktop/views/pages/user/user.profile.vue | 18 ++++------
 src/web/app/init.ts                           | 34 ++++++++++--------
 .../mobile/views/components/post-detail.vue   | 24 ++++++-------
 .../mobile/views/components/posts.post.vue    | 24 ++++++-------
 src/web/app/mobile/views/components/ui.vue    |  8 ++---
 14 files changed, 102 insertions(+), 126 deletions(-)

diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index e3a66f5b1..e20f4bfe4 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -67,6 +67,16 @@ export default class MiOS extends EventEmitter {
 
 	private isMetaFetching = false;
 
+	public app: Vue;
+
+	public new(vm, props) {
+		const w = new vm({
+			parent: this.app,
+			propsData: props
+		}).$mount();
+		document.body.appendChild(w.$el);
+	}
+
 	/**
 	 * A signing user
 	 */
diff --git a/src/web/app/desktop/views/components/drive.vue b/src/web/app/desktop/views/components/drive.vue
index e256bc6af..0dcf07701 100644
--- a/src/web/app/desktop/views/components/drive.vue
+++ b/src/web/app/desktop/views/components/drive.vue
@@ -376,11 +376,9 @@ export default Vue.extend({
 		},
 
 		newWindow(folder) {
-			document.body.appendChild(new MkDriveWindow({
-				propsData: {
-					folder: folder
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkDriveWindow, {
+				folder: folder
+			});
 		},
 
 		move(target) {
diff --git a/src/web/app/desktop/views/components/images-image.vue b/src/web/app/desktop/views/components/images-image.vue
index cb6c529f7..5b7dc4173 100644
--- a/src/web/app/desktop/views/components/images-image.vue
+++ b/src/web/app/desktop/views/components/images-image.vue
@@ -39,11 +39,9 @@ export default Vue.extend({
 		},
 
 		onClick() {
-			document.body.appendChild(new MkImagesImageDialog({
-				propsData: {
-					image: this.image
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkImagesImageDialog, {
+				image: this.image
+			});
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/messaging-window.vue b/src/web/app/desktop/views/components/messaging-window.vue
index eeeb97e34..ac2746598 100644
--- a/src/web/app/desktop/views/components/messaging-window.vue
+++ b/src/web/app/desktop/views/components/messaging-window.vue
@@ -12,11 +12,9 @@ import MkMessagingRoomWindow from './messaging-room-window.vue';
 export default Vue.extend({
 	methods: {
 		navigate(user) {
-			document.body.appendChild(new MkMessagingRoomWindow({
-				propsData: {
-					user: user
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkMessagingRoomWindow, {
+				user: user
+			});
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index cac4671c5..c453867df 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -148,34 +148,26 @@ export default Vue.extend({
 			});
 		},
 		reply() {
-			document.body.appendChild(new MkPostFormWindow({
-				propsData: {
-					reply: this.p
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkPostFormWindow, {
+				reply: this.p
+			});
 		},
 		repost() {
-			document.body.appendChild(new MkRepostFormWindow({
-				propsData: {
-					post: this.p
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkRepostFormWindow, {
+				post: this.p
+			});
 		},
 		react() {
-			document.body.appendChild(new MkReactionPicker({
-				propsData: {
-					source: this.$refs.reactButton,
-					post: this.p
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				post: this.p
+			});
 		},
 		menu() {
-			document.body.appendChild(new MkPostMenu({
-				propsData: {
-					source: this.$refs.menuButton,
-					post: this.p
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkPostMenu, {
+				source: this.$refs.menuButton,
+				post: this.p
+			});
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/posts.post.vue b/src/web/app/desktop/views/components/posts.post.vue
index 24f7b5e12..6fe097909 100644
--- a/src/web/app/desktop/views/components/posts.post.vue
+++ b/src/web/app/desktop/views/components/posts.post.vue
@@ -186,34 +186,26 @@ export default Vue.extend({
 			}
 		},
 		reply() {
-			document.body.appendChild(new MkPostFormWindow({
-				propsData: {
-					reply: this.p
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkPostFormWindow, {
+				reply: this.p
+			});
 		},
 		repost() {
-			document.body.appendChild(new MkRepostFormWindow({
-				propsData: {
-					post: this.p
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkRepostFormWindow, {
+				post: this.p
+			});
 		},
 		react() {
-			document.body.appendChild(new MkReactionPicker({
-				propsData: {
-					source: this.$refs.reactButton,
-					post: this.p
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				post: this.p
+			});
 		},
 		menu() {
-			document.body.appendChild(new MkPostMenu({
-				propsData: {
-					source: this.$refs.menuButton,
-					post: this.p
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkPostMenu, {
+				source: this.$refs.menuButton,
+				post: this.p
+			});
 		},
 		onKeydown(e) {
 			let shouldBeCancel = true;
diff --git a/src/web/app/desktop/views/components/ui.header.account.vue b/src/web/app/desktop/views/components/ui.header.account.vue
index 3728f94be..af58e81a0 100644
--- a/src/web/app/desktop/views/components/ui.header.account.vue
+++ b/src/web/app/desktop/views/components/ui.header.account.vue
@@ -70,11 +70,11 @@ export default Vue.extend({
 		},
 		drive() {
 			this.close();
-			document.body.appendChild(new MkDriveWindow().$mount().$el);
+			(this as any).os.new(MkDriveWindow);
 		},
 		settings() {
 			this.close();
-			document.body.appendChild(new MkSettingsWindow().$mount().$el);
+			(this as any).os.new(MkSettingsWindow);
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/ui.header.nav.vue b/src/web/app/desktop/views/components/ui.header.nav.vue
index 70c616d9c..c102d5b3f 100644
--- a/src/web/app/desktop/views/components/ui.header.nav.vue
+++ b/src/web/app/desktop/views/components/ui.header.nav.vue
@@ -79,7 +79,7 @@ export default Vue.extend({
 		},
 
 		messaging() {
-			document.body.appendChild(new MkMessagingWindow().$mount().$el);
+			(this as any).os.new(MkMessagingWindow);
 		}
 	}
 });
diff --git a/src/web/app/desktop/views/components/widgets/messaging.vue b/src/web/app/desktop/views/components/widgets/messaging.vue
index e510a07dc..ae7d6934a 100644
--- a/src/web/app/desktop/views/components/widgets/messaging.vue
+++ b/src/web/app/desktop/views/components/widgets/messaging.vue
@@ -17,11 +17,9 @@ export default define({
 }).extend({
 	methods: {
 		navigate(user) {
-			document.body.appendChild(new MkMessagingRoomWindow({
-				propsData: {
-					user: user
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkMessagingRoomWindow, {
+				user: user
+			});
 		},
 		func() {
 			if (this.props.design == 1) {
diff --git a/src/web/app/desktop/views/pages/user/user.profile.vue b/src/web/app/desktop/views/pages/user/user.profile.vue
index b55787c95..ceca829ac 100644
--- a/src/web/app/desktop/views/pages/user/user.profile.vue
+++ b/src/web/app/desktop/views/pages/user/user.profile.vue
@@ -36,21 +36,15 @@ export default Vue.extend({
 	},
 	methods: {
 		showFollowing() {
-			document.body.appendChild(new MkFollowingWindow({
-				parent: this,
-				propsData: {
-					user: this.user
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkFollowingWindow, {
+				user: this.user
+			});
 		},
 
 		showFollowers() {
-			document.body.appendChild(new MkFollowersWindow({
-				parent: this,
-				propsData: {
-					user: this.user
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkFollowersWindow, {
+				user: this.user
+			});
 		},
 
 		mute() {
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index e4cb8f8bc..ac567c502 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -87,8 +87,26 @@ export default (callback: (launch: (api: (os: MiOS) => API) => [Vue, MiOS]) => v
 		// アプリ基底要素マウント
 		document.body.innerHTML = '<div id="app"></div>';
 
+		const app = new Vue({
+			router: new VueRouter({
+				mode: 'history'
+			}),
+			created() {
+				this.$watch('os.i', i => {
+					// キャッシュ更新
+					localStorage.setItem('me', JSON.stringify(i));
+				}, {
+					deep: true
+				});
+			},
+			render: createEl => createEl(App)
+		});
+
+		os.app = app;
+
 		const launch = (api: (os: MiOS) => API) => {
 			os.apis = api(os);
+
 			Vue.mixin({
 				data() {
 					return {
@@ -99,20 +117,8 @@ export default (callback: (launch: (api: (os: MiOS) => API) => [Vue, MiOS]) => v
 				}
 			});
 
-			const app = new Vue({
-				router: new VueRouter({
-					mode: 'history'
-				}),
-				created() {
-					this.$watch('os.i', i => {
-						// キャッシュ更新
-						localStorage.setItem('me', JSON.stringify(i));
-					}, {
-						deep: true
-					});
-				},
-				render: createEl => createEl(App)
-			}).$mount('#app');
+			// マウント
+			app.$mount('#app');
 
 			return [app, os] as [Vue, MiOS];
 		};
diff --git a/src/web/app/mobile/views/components/post-detail.vue b/src/web/app/mobile/views/components/post-detail.vue
index 76057525f..e7c08df7e 100644
--- a/src/web/app/mobile/views/components/post-detail.vue
+++ b/src/web/app/mobile/views/components/post-detail.vue
@@ -154,22 +154,18 @@ export default Vue.extend({
 			});
 		},
 		react() {
-			document.body.appendChild(new MkReactionPicker({
-				propsData: {
-					source: this.$refs.reactButton,
-					post: this.p,
-					compact: true
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				post: this.p,
+				compact: true
+			});
 		},
 		menu() {
-			document.body.appendChild(new MkPostMenu({
-				propsData: {
-					source: this.$refs.menuButton,
-					post: this.p,
-					compact: true
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkPostMenu, {
+				source: this.$refs.menuButton,
+				post: this.p,
+				compact: true
+			});
 		}
 	}
 });
diff --git a/src/web/app/mobile/views/components/posts.post.vue b/src/web/app/mobile/views/components/posts.post.vue
index 9a7d633d4..43d8d4a89 100644
--- a/src/web/app/mobile/views/components/posts.post.vue
+++ b/src/web/app/mobile/views/components/posts.post.vue
@@ -169,22 +169,18 @@ export default Vue.extend({
 			});
 		},
 		react() {
-			document.body.appendChild(new MkReactionPicker({
-				propsData: {
-					source: this.$refs.reactButton,
-					post: this.p,
-					compact: true
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				post: this.p,
+				compact: true
+			});
 		},
 		menu() {
-			document.body.appendChild(new MkPostMenu({
-				propsData: {
-					source: this.$refs.menuButton,
-					post: this.p,
-					compact: true
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkPostMenu, {
+				source: this.$refs.menuButton,
+				post: this.p,
+				compact: true
+			});
 		}
 	}
 });
diff --git a/src/web/app/mobile/views/components/ui.vue b/src/web/app/mobile/views/components/ui.vue
index 1e34c84e6..54b8a2d0d 100644
--- a/src/web/app/mobile/views/components/ui.vue
+++ b/src/web/app/mobile/views/components/ui.vue
@@ -53,11 +53,9 @@ export default Vue.extend({
 				id: notification.id
 			});
 
-			document.body.appendChild(new MkNotify({
-				propsData: {
-					notification
-				}
-			}).$mount().$el);
+			(this as any).os.new(MkNotify, {
+				notification
+			});
 		}
 	}
 });

From 1fdc805713010b39476b7a68b14899103600e6c8 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 01:11:09 +0900
Subject: [PATCH 282/286] wip

---
 src/web/app/desktop/views/components/notifications.vue | 2 +-
 src/web/app/desktop/views/components/user-preview.vue  | 9 +++++----
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/src/web/app/desktop/views/components/notifications.vue b/src/web/app/desktop/views/components/notifications.vue
index e3a69d620..bcd7cf35f 100644
--- a/src/web/app/desktop/views/components/notifications.vue
+++ b/src/web/app/desktop/views/components/notifications.vue
@@ -234,7 +234,7 @@ export default Vue.extend({
 				p
 					margin 0
 
-					i, mk-reaction-icon
+					i, .mk-reaction-icon
 						margin-right 4px
 
 			.post-preview
diff --git a/src/web/app/desktop/views/components/user-preview.vue b/src/web/app/desktop/views/components/user-preview.vue
index df2c7e897..2a4bd7cf7 100644
--- a/src/web/app/desktop/views/components/user-preview.vue
+++ b/src/web/app/desktop/views/components/user-preview.vue
@@ -2,11 +2,11 @@
 <div class="mk-user-preview">
 	<template v-if="u != null">
 		<div class="banner" :style="u.banner_url ? `background-image: url(${u.banner_url}?thumbnail&size=512)` : ''"></div>
-		<a class="avatar" :href="`/${u.username}`" target="_blank">
+		<router-link class="avatar" :to="`/${u.username}`">
 			<img :src="`${u.avatar_url}?thumbnail&size=64`" alt="avatar"/>
-		</a>
+		</router-link>
 		<div class="title">
-			<p class="name">{{ u.name }}</p>
+			<router-link class="name" :to="`/${u.username}`">{{ u.name }}</router-link>
 			<p class="username">@{{ u.username }}</p>
 		</div>
 		<div class="description">{{ u.description }}</div>
@@ -106,6 +106,7 @@ export default Vue.extend({
 		position absolute
 		top 62px
 		left 13px
+		z-index 2
 
 		> img
 			display block
@@ -120,7 +121,7 @@ export default Vue.extend({
 		padding 8px 0 8px 82px
 
 		> .name
-			display block
+			display inline-block
 			margin 0
 			font-weight bold
 			line-height 16px

From adaa07a6f9c073078654b4c64d6fad5da4482ecd Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 01:19:51 +0900
Subject: [PATCH 283/286] wip

---
 src/web/app/common/views/components/messaging.vue | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index 9f04f8933..6dc19b874 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -21,12 +21,13 @@
 		</div>
 	</div>
 	<div class="history" v-if="messages.length > 0">
-		<template >
+		<template>
 			<a v-for="message in messages"
 				class="user"
+				:href="`/i/messaging/${isMe(message) ? message.recipient.username : message.user.username}`"
 				:data-is-me="isMe(message)"
 				:data-is-read="message.is_read"
-				@click="navigate(isMe(message) ? message.recipient : message.user)"
+				@click.prevent="navigate(isMe(message) ? message.recipient : message.user)"
 				:key="message.id"
 			>
 				<div>
@@ -220,13 +221,13 @@ export default Vue.extend({
 					bottom 0
 					left 0
 					width 1em
-					height 1em
+					line-height 56px
 					margin auto
 					color #555
 
 			> input
 				margin 0
-				padding 0 0 0 38px
+				padding 0 0 0 32px
 				width 100%
 				font-size 1em
 				line-height 38px

From e0ffedca240309bada9dcd7e53e66cc804e2912d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 01:27:02 +0900
Subject: [PATCH 284/286] wip

---
 src/api/private/signup.ts                     |  6 ++--
 .../app/desktop/views/components/dialog.vue   | 29 ++++++++++---------
 .../desktop/views/components/post-preview.vue |  1 -
 3 files changed, 19 insertions(+), 17 deletions(-)

diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 8efdb6db4..19e331475 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -15,7 +15,7 @@ const home = {
 		'profile',
 		'calendar',
 		'activity',
-		'rss-reader',
+		'rss',
 		'trends',
 		'photo-stream',
 		'version'
@@ -23,8 +23,8 @@ const home = {
 	right: [
 		'broadcast',
 		'notifications',
-		'user-recommendation',
-		'recommended-polls',
+		'users',
+		'polls',
 		'server',
 		'donation',
 		'nav',
diff --git a/src/web/app/desktop/views/components/dialog.vue b/src/web/app/desktop/views/components/dialog.vue
index f089b19a4..28f22f7b6 100644
--- a/src/web/app/desktop/views/components/dialog.vue
+++ b/src/web/app/desktop/views/components/dialog.vue
@@ -2,7 +2,7 @@
 <div class="mk-dialog">
 	<div class="bg" ref="bg" @click="onBgClick"></div>
 	<div class="main" ref="main">
-		<header v-html="title"></header>
+		<header v-html="title" :class="$style.header"></header>
 		<div class="body" v-html="text"></div>
 		<div class="buttons">
 			<button v-for="button in buttons" @click="click(button)">{{ button.text }}</button>
@@ -110,18 +110,6 @@ export default Vue.extend({
 		background #fff
 		opacity 0
 
-		> header
-			margin 1em 0
-			color $theme-color
-			// color #43A4EC
-			font-weight bold
-
-			&:empty
-				display none
-
-			> i
-				margin-right 0.5em
-
 		> .body
 			margin 1em 0
 			color #888
@@ -154,3 +142,18 @@ export default Vue.extend({
 					transition color 0s ease
 
 </style>
+
+<style lang="stylus" module>
+.header
+	margin 1em 0
+	color $theme-color
+	// color #43A4EC
+	font-weight bold
+
+	&:empty
+		display none
+
+	> i
+		margin-right 0.5em
+
+</style>
diff --git a/src/web/app/desktop/views/components/post-preview.vue b/src/web/app/desktop/views/components/post-preview.vue
index b39ad3db4..6a0a60e4a 100644
--- a/src/web/app/desktop/views/components/post-preview.vue
+++ b/src/web/app/desktop/views/components/post-preview.vue
@@ -64,7 +64,6 @@ export default Vue.extend({
 
 		> header
 			display flex
-			margin 4px 0
 			white-space nowrap
 
 			> .name

From c686a1047248749b21c76fd9f5d867c9324cdd82 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 02:06:35 +0900
Subject: [PATCH 285/286] wip

---
 src/api/endpoints.ts                          |  5 ++
 src/api/endpoints/i/update.ts                 |  8 +--
 src/api/endpoints/i/update_client_setting.ts  | 43 +++++++++++++++
 .../app/common/views/components/post-html.ts  |  6 ++-
 src/web/app/desktop/views/components/home.vue | 54 +++++++++++--------
 .../views/components/settings-window.vue      | 12 +++--
 .../app/desktop/views/components/settings.vue | 33 +++++++++++-
 7 files changed, 127 insertions(+), 34 deletions(-)
 create mode 100644 src/api/endpoints/i/update_client_setting.ts

diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index e84638157..ff214c300 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -194,6 +194,11 @@ const endpoints: Endpoint[] = [
 		withCredential: true,
 		secure: true
 	},
+	{
+		name: 'i/update_client_setting',
+		withCredential: true,
+		secure: true
+	},
 	{
 		name: 'i/pin',
 		kind: 'account-write'
diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts
index 7bbbf9590..43c524504 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/api/endpoints/i/update.ts
@@ -46,19 +46,13 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
 	if (bannerIdErr) return rej('invalid banner_id param');
 	if (bannerId) user.banner_id = bannerId;
 
-	// Get 'show_donation' parameter
-	const [showDonation, showDonationErr] = $(params.show_donation).optional.boolean().$;
-	if (showDonationErr) return rej('invalid show_donation param');
-	if (showDonation) user.client_settings.show_donation = showDonation;
-
 	await User.update(user._id, {
 		$set: {
 			name: user.name,
 			description: user.description,
 			avatar_id: user.avatar_id,
 			banner_id: user.banner_id,
-			profile: user.profile,
-			'client_settings.show_donation': user.client_settings.show_donation
+			profile: user.profile
 		}
 	});
 
diff --git a/src/api/endpoints/i/update_client_setting.ts b/src/api/endpoints/i/update_client_setting.ts
new file mode 100644
index 000000000..b817ff354
--- /dev/null
+++ b/src/api/endpoints/i/update_client_setting.ts
@@ -0,0 +1,43 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import User, { pack } from '../../models/user';
+import event from '../../event';
+
+/**
+ * Update myself
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = async (params, user) => new Promise(async (res, rej) => {
+	// Get 'name' parameter
+	const [name, nameErr] = $(params.name).string().$;
+	if (nameErr) return rej('invalid name param');
+
+	// Get 'value' parameter
+	const [value, valueErr] = $(params.value).nullable.any().$;
+	if (valueErr) return rej('invalid value param');
+
+	const x = {};
+	x[`client_settings.${name}`] = value;
+
+	await User.update(user._id, {
+		$set: x
+	});
+
+	// Serialize
+	user.client_settings[name] = value;
+	const iObj = await pack(user, user, {
+		detail: true,
+		includeSecrets: true
+	});
+
+	// Send response
+	res(iObj);
+
+	// Publish i updated event
+	event(user._id, 'i_updated', iObj);
+});
diff --git a/src/web/app/common/views/components/post-html.ts b/src/web/app/common/views/components/post-html.ts
index afd95f8e3..16d670e85 100644
--- a/src/web/app/common/views/components/post-html.ts
+++ b/src/web/app/common/views/components/post-html.ts
@@ -33,7 +33,11 @@ export default Vue.component('mk-post-html', {
 						.replace(/(\r\n|\n|\r)/g, '\n');
 
 					if ((this as any).shouldBreak) {
-						return text.split('\n').map(t => [createElement('span', t), createElement('br')]);
+						if (text.indexOf('\n') != -1) {
+							return text.split('\n').map(t => [createElement('span', t), createElement('br')]);
+						} else {
+							return createElement('span', text);
+						}
 					} else {
 						return createElement('span', text.replace(/\n/g, ' '));
 					}
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index eabcc485d..8a61c378e 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-home" :data-customize="customize">
 	<div class="customize" v-if="customize">
-		<a href="/">%fa:check%完了</a>
+		<router-link to="/">%fa:check%完了</router-link>
 		<div>
 			<div class="adder">
 				<p>ウィジェットを追加:</p>
@@ -51,7 +51,11 @@
 				</div>
 			</x-draggable>
 			<div class="main">
-				<mk-timeline ref="tl" @loaded="onTlLoaded"/>
+				<a @click="hint">カスタマイズのヒント</a>
+				<div>
+					<mk-post-form v-if="os.i.client_settings.showPostFormOnTopOfTl"/>
+					<mk-timeline ref="tl" @loaded="onTlLoaded"/>
+				</div>
 			</div>
 		</template>
 		<template v-else>
@@ -59,6 +63,7 @@
 				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" @chosen="warp"/>
 			</div>
 			<div class="main">
+				<mk-post-form v-if="os.i.client_settings.showPostFormOnTopOfTl"/>
 				<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
 				<mk-mentions @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
 			</div>
@@ -126,23 +131,19 @@ export default Vue.extend({
 			deep: true
 		});
 	},
-	mounted() {
-		this.$nextTick(() => {
-			if (this.customize) {
-				(this as any).apis.dialog({
-					title: '%fa:info-circle%カスタマイズのヒント',
-					text: '<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' +
-						'<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' +
-						'<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' +
-						'<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>',
-					actions: [{
-						text: 'Got it!'
-					}]
-				});
-			}
-		});
-	},
 	methods: {
+		hint() {
+			(this as any).apis.dialog({
+				title: '%fa:info-circle%カスタマイズのヒント',
+				text: '<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p>' +
+					'<p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p>' +
+					'<p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p>' +
+					'<p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>',
+				actions: [{
+					text: 'Got it!'
+				}]
+			});
+		},
 		onTlLoaded() {
 			this.$emit('loaded');
 		},
@@ -193,10 +194,16 @@ export default Vue.extend({
 		background-image url('/assets/desktop/grid.svg')
 
 		> .main > .main
-			cursor not-allowed !important
+			> a
+				display block
+				margin-bottom 8px
+				text-align center
 
-			> *
-				pointer-events none
+			> div
+				cursor not-allowed !important
+
+				> *
+					pointer-events none
 
 	&:not([data-customize])
 		> .main > *:empty
@@ -287,6 +294,11 @@ export default Vue.extend({
 			width calc(100% - 275px * 2)
 			order 2
 
+			.mk-post-form
+				margin-bottom 16px
+				border solid 1px #e5e5e5
+				border-radius 4px
+
 		> *:not(.main)
 			width 275px
 			padding 16px 0 16px 0
diff --git a/src/web/app/desktop/views/components/settings-window.vue b/src/web/app/desktop/views/components/settings-window.vue
index c4e1d6a0a..d5be177dc 100644
--- a/src/web/app/desktop/views/components/settings-window.vue
+++ b/src/web/app/desktop/views/components/settings-window.vue
@@ -1,13 +1,19 @@
 <template>
-<mk-window is-modal width='700px' height='550px' @closed="$destroy">
+<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">%fa:cog%設定</span>
-	<mk-settings/>
+	<mk-settings @done="close"/>
 </mk-window>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-export default Vue.extend({});
+export default Vue.extend({
+	methods: {
+		close() {
+			(this as any).$refs.window.close();
+		}
+	}
+});
 </script>
 
 <style lang="stylus" module>
diff --git a/src/web/app/desktop/views/components/settings.vue b/src/web/app/desktop/views/components/settings.vue
index 767ec3f96..c210997c3 100644
--- a/src/web/app/desktop/views/components/settings.vue
+++ b/src/web/app/desktop/views/components/settings.vue
@@ -20,7 +20,13 @@
 
 		<section class="web" v-show="page == 'web'">
 			<h1>デザイン</h1>
-			<a href="/i/customize-home" class="ui button">ホームをカスタマイズ</a>
+			<div>
+				<button class="ui button" @click="customizeHome">ホームをカスタマイズ</button>
+			</div>
+			<label>
+				<input type="checkbox" v-model="showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl">
+				<span>タイムライン上部に投稿フォームを表示する</span>
+			</label>
 		</section>
 
 		<section class="drive" v-show="page == 'drive'">
@@ -89,8 +95,25 @@ export default Vue.extend({
 	},
 	data() {
 		return {
-			page: 'profile'
+			page: 'profile',
+
+			showPostFormOnTopOfTl: false
 		};
+	},
+	created() {
+		this.showPostFormOnTopOfTl = (this as any).os.i.client_settings.showPostFormOnTopOfTl;
+	},
+	methods: {
+		customizeHome() {
+			this.$router.push('/i/customize-home');
+			this.$emit('done');
+		},
+		onChangeShowPostFormOnTopOfTl() {
+			(this as any).api('i/update_client_setting', {
+				name: 'showPostFormOnTopOfTl',
+				value: this.showPostFormOnTopOfTl
+			});
+		}
 	}
 });
 </script>
@@ -146,4 +169,10 @@ export default Vue.extend({
 				color #555
 				border-bottom solid 1px #eee
 
+		> .web
+			> div
+				border-bottom solid 1px #eee
+				padding 0 0 16px 0
+				margin 0 0 16px 0
+
 </style>

From 3725d444f101ce14bfc007bf07f831b04b9bd449 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Fri, 23 Feb 2018 02:09:48 +0900
Subject: [PATCH 286/286] wip

---
 src/web/app/desktop/views/components/post-form.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/app/desktop/views/components/post-form.vue b/src/web/app/desktop/views/components/post-form.vue
index 1c152910e..d38ed9a04 100644
--- a/src/web/app/desktop/views/components/post-form.vue
+++ b/src/web/app/desktop/views/components/post-form.vue
@@ -22,7 +22,7 @@
 		</div>
 		<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="saveDraft()"/>
 	</div>
-	<mk-uploader @uploaded="attachMedia" @change="onChangeUploadings"/>
+	<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
 	<button class="upload" title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" @click="chooseFile">%fa:upload%</button>
 	<button class="drive" title="%i18n:desktop.tags.mk-post-form.attach-media-from-drive%" @click="chooseFileFromDrive">%fa:cloud%</button>
 	<button class="kao" title="%i18n:desktop.tags.mk-post-form.insert-a-kao%" @click="kao">%fa:R smile%</button>